UNPKG

@thoughtspot/visual-embed-sdk

Version:
523 lines 19.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isAuthenticated = exports.authenticate = exports.logout = exports.doOIDCAuth = exports.doSamlAuth = exports.doBasicAuth = exports.doCookielessTokenAuth = exports.doTokenAuth = exports.getReleaseVersion = exports.postLoginService = exports.notifyLogout = exports.notifyAuthFailure = exports.notifyAuthSuccess = exports.notifyAuthSDKSuccess = exports.setAuthEE = exports.getAuthEE = exports.AuthEvent = exports.AuthStatus = exports.AuthFailureType = exports.SSO_REDIRECTION_MARKER_GUID = exports.samlCompletionPromise = exports.samlAuthWindow = exports.loggedInStatus = void 0; const authToken_1 = require("./authToken"); const embedConfig_1 = require("./embed/embedConfig"); const mixpanel_service_1 = require("./mixpanel-service"); const types_1 = require("./types"); const utils_1 = require("./utils"); const authService_1 = require("./utils/authService"); const tokenizedAuthService_1 = require("./utils/authService/tokenizedAuthService"); const logger_1 = require("./utils/logger"); const sessionInfoService_1 = require("./utils/sessionInfoService"); const errors_1 = require("./errors"); const resetServices_1 = require("./utils/resetServices"); exports.loggedInStatus = false; exports.samlAuthWindow = null; exports.samlCompletionPromise = null; let releaseVersion = ''; exports.SSO_REDIRECTION_MARKER_GUID = '5e16222e-ef02-43e9-9fbd-24226bf3ce5b'; /** * Enum for auth failure types. * This value is passed to the listener for {@link AuthStatus.FAILURE}. * @group Authentication / Init */ var AuthFailureType; (function (AuthFailureType) { /** * Authentication failed in the SDK authentication flow. * * Emitted when `init()` or auto-authentication cannot establish a logged-in session. * For example, this can happen because of an invalid token, an auth request failure, * or an auth promise rejection. */ AuthFailureType["SDK"] = "SDK"; /** * Browser cookie access is blocked for the embedded app. * * Emitted when the iframe reports that required cookies * cannot be read or sent, commonly due to third-party cookie restrictions. */ AuthFailureType["NO_COOKIE_ACCESS"] = "NO_COOKIE_ACCESS"; /** * The current authentication token or session has expired. * * Emitted when the embed receives an auth-expiry signal and starts auth refresh * handling. */ AuthFailureType["EXPIRY"] = "EXPIRY"; /** * A generic authentication failure that does not match a more specific type. * * Emitted as a fallback for app-reported auth failures in standard auth flows. */ AuthFailureType["OTHER"] = "OTHER"; /** * The user session timed out due to inactivity. * * Emitted when the app reports an idle-session timeout. */ AuthFailureType["IDLE_SESSION_TIMEOUT"] = "IDLE_SESSION_TIMEOUT"; /** * The app reports that the user is unauthenticated. * * Used primarily to classify unauthenticated failures in Embedded SSO flows. */ AuthFailureType["UNAUTHENTICATED_FAILURE"] = "UNAUTHENTICATED_FAILURE"; })(AuthFailureType = exports.AuthFailureType || (exports.AuthFailureType = {})); /** * Enum for auth status emitted by the emitter returned from {@link init}. * @group Authentication / Init */ var AuthStatus; (function (AuthStatus) { /** * Emits when the SDK fails to authenticate. */ AuthStatus["FAILURE"] = "FAILURE"; /** * Emits when the SDK authentication step completes * successfully (e.g., token exchange, cookie set). * This fires before any iframe is rendered. Use * this to know that auth passed and it is safe to * proceed with rendering. The callback receives no * arguments. * @example * ```js * const authEE = init({ ... }); * authEE.on(AuthStatus.SDK_SUCCESS, () => { * // Auth done, iframe not loaded yet * }); * ``` */ AuthStatus["SDK_SUCCESS"] = "SDK_SUCCESS"; /** * @hidden * Emits when iframe is loaded and session * information is available. */ AuthStatus["SESSION_INFO_SUCCESS"] = "SESSION_INFO_SUCCESS"; /** * Emits when the ThoughtSpot app inside the * embedded iframe confirms its session is active. * This fires after the iframe loads and sends back an `AuthInit` event. * @param sessionInfo Information about the user session, with details like `userGUID`. * @see EmbedEvent.AuthInit * @example * ```js * const authEE = init({ ... }); * authEE.on(AuthStatus.SUCCESS, (sessionInfo) => { * // App is loaded and authenticated * console.log(sessionInfo.userGUID); * }); * ``` */ 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 = exports.AuthStatus || (exports.AuthStatus = {})); /** * Events which can be triggered on the emitter returned from {@link init}. * @group Authentication / Init */ 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 = exports.AuthEvent || (exports.AuthEvent = {})); let authEE; /** * */ function getAuthEE() { return authEE; } exports.getAuthEE = getAuthEE; /** * * @param eventEmitter */ function setAuthEE(eventEmitter) { authEE = eventEmitter; } exports.setAuthEE = setAuthEE; /** * */ function notifyAuthSDKSuccess() { if (!authEE) { logger_1.logger.error(errors_1.ERROR_MESSAGE.SDK_NOT_INITIALIZED); return; } authEE.emit(AuthStatus.SDK_SUCCESS); } exports.notifyAuthSDKSuccess = notifyAuthSDKSuccess; /** * */ async function notifyAuthSuccess() { if (!authEE) { logger_1.logger.error(errors_1.ERROR_MESSAGE.SDK_NOT_INITIALIZED); return; } try { (0, sessionInfoService_1.getPreauthInfo)(); const sessionInfo = await (0, sessionInfoService_1.getSessionInfo)(); authEE.emit(AuthStatus.SUCCESS, sessionInfo); } catch (e) { logger_1.logger.error(errors_1.ERROR_MESSAGE.SESSION_INFO_FAILED); } } exports.notifyAuthSuccess = notifyAuthSuccess; /** * * @param failureType */ function notifyAuthFailure(failureType) { if (!authEE) { logger_1.logger.error(errors_1.ERROR_MESSAGE.SDK_NOT_INITIALIZED); return; } authEE.emit(AuthStatus.FAILURE, failureType); } exports.notifyAuthFailure = notifyAuthFailure; /** * */ function notifyLogout() { if (!authEE) { logger_1.logger.error(errors_1.ERROR_MESSAGE.SDK_NOT_INITIALIZED); return; } authEE.emit(AuthStatus.LOGOUT); } exports.notifyLogout = notifyLogout; /** * 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 (0, tokenizedAuthService_1.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: * */ async function postLoginService() { try { (0, sessionInfoService_1.getPreauthInfo)(); const sessionInfo = await (0, sessionInfoService_1.getSessionInfo)(); releaseVersion = sessionInfo.releaseVersion; const embedConfig = (0, embedConfig_1.getEmbedConfig)(); if (!embedConfig.disableSDKTracking) { (0, mixpanel_service_1.initMixpanel)(sessionInfo); } } catch (e) { logger_1.logger.error('Post login services failed.', e.message, e); } } exports.postLoginService = postLoginService; /** * Return releaseVersion if available */ function getReleaseVersion() { return releaseVersion; } exports.getReleaseVersion = getReleaseVersion; /** * Check if we are stuck at the SSO redirect URL */ function isAtSSORedirectUrl() { return window.location.href.indexOf((0, utils_1.getSSOMarker)(exports.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 occurrences of ?ssoMarker=guid or &ssoMarker=guid. let updatedHash = window.location.hash.replace(`?${(0, utils_1.getSSOMarker)(exports.SSO_REDIRECTION_MARKER_GUID)}`, ''); updatedHash = updatedHash.replace(`&${(0, utils_1.getSSOMarker)(exports.SSO_REDIRECTION_MARKER_GUID)}`, ''); window.location.hash = updatedHash; } /** * Perform token based authentication * @param embedConfig The embed configuration */ 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'); } exports.loggedInStatus = await isLoggedIn(thoughtSpotHost); if (!exports.loggedInStatus) { let authToken; try { authToken = await (0, authToken_1.getAuthenticationToken)(embedConfig); } catch (e) { exports.loggedInStatus = false; throw e; } let resp; try { resp = await (0, authService_1.fetchAuthPostService)(thoughtSpotHost, username, authToken); } catch (e) { resp = await (0, authService_1.fetchAuthService)(thoughtSpotHost, username, authToken); } // token login issues a 302 when successful exports.loggedInStatus = resp.ok || resp.type === 'opaqueredirect'; if (exports.loggedInStatus && embedConfig.detectCookieAccessSlow) { // When 3rd party cookie access is blocked, this will fail because // cookies will not be sent with the call. exports.loggedInStatus = await isLoggedIn(thoughtSpotHost); } } return exports.loggedInStatus; }; exports.doTokenAuth = doTokenAuth; /** * Validate embedConfig parameters required for cookielessTokenAuth * @param embedConfig The embed configuration */ 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 (0, authToken_1.getAuthenticationToken)(embedConfig); if (authToken) { authSuccess = true; } } catch { authSuccess = false; } return authSuccess; }; exports.doCookielessTokenAuth = doCookielessTokenAuth; /** * 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 */ const doBasicAuth = async (embedConfig) => { const { thoughtSpotHost, username, password } = embedConfig; const loggedIn = await isLoggedIn(thoughtSpotHost); if (!loggedIn) { const response = await (0, authService_1.fetchBasicAuthService)(thoughtSpotHost, username, password); exports.loggedInStatus = response.ok; if (embedConfig.detectCookieAccessSlow) { exports.loggedInStatus = await isLoggedIn(thoughtSpotHost); } } else { exports.loggedInStatus = true; } return exports.loggedInStatus; }; exports.doBasicAuth = doBasicAuth; /** * * @param ssoURL * @param triggerContainer * @param triggerText */ async function samlPopupFlow(ssoURL, triggerContainer, triggerText) { let popupClosedCheck; const openPopup = () => { if (exports.samlAuthWindow === null || exports.samlAuthWindow.closed) { exports.samlAuthWindow = window.open(ssoURL, '_blank', 'location=no,height=570,width=520,scrollbars=yes,status=yes'); if (exports.samlAuthWindow) { popupClosedCheck = setInterval(() => { if (exports.samlAuthWindow.closed) { clearInterval(popupClosedCheck); if (exports.samlCompletionPromise && !samlCompletionResolved) { authEE === null || authEE === void 0 ? void 0 : authEE.emit(AuthStatus.SAML_POPUP_CLOSED_NO_AUTH); } } }, 500); } } else { exports.samlAuthWindow.focus(); } }; let samlCompletionResolved = false; authEE === null || authEE === void 0 ? void 0 : authEE.emit(AuthStatus.WAITING_FOR_POPUP); const containerEl = (0, utils_1.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 }); } exports.samlCompletionPromise = exports.samlCompletionPromise || new Promise((resolve, reject) => { window.addEventListener('message', (e) => { if (e.data.type === types_1.EmbedEvent.SAMLComplete) { if (e.data.accessToken) { const decodedToken = decodeURIComponent(e.data.accessToken); (0, authToken_1.storeAuthTokenInCache)(decodedToken); } 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 exports.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(); } exports.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(); exports.loggedInStatus = false; return; } const ssoURL = `${thoughtSpotHost}${ssoEndPoint}`; if (embedConfig.inPopup) { await samlPopupFlow(ssoURL, embedConfig.authTriggerContainer, embedConfig.authTriggerText); const cachedToken = (0, authToken_1.getCacheAuthToken)(); if (cachedToken) { exports.loggedInStatus = true; } else { exports.loggedInStatus = await isLoggedIn(thoughtSpotHost); } return; } window.location.href = ssoURL; }; 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` : (0, utils_1.getRedirectUrl)(window.location.href, exports.SSO_REDIRECTION_MARKER_GUID, embedConfig.redirectPath); // bring back the page to the same URL const ssoEndPoint = `${authService_1.EndPoints.SAML_LOGIN_TEMPLATE(encodeURIComponent(ssoRedirectUrl))}`; await doSSOAuth(embedConfig, ssoEndPoint); return exports.loggedInStatus; }; exports.doSamlAuth = doSamlAuth; 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` : (0, utils_1.getRedirectUrl)(window.location.href, exports.SSO_REDIRECTION_MARKER_GUID, embedConfig.redirectPath); // bring back the page to the same URL const baseEndpoint = `${authService_1.EndPoints.OIDC_LOGIN_TEMPLATE(encodeURIComponent(ssoRedirectUrl))}`; const ssoEndPoint = `${baseEndpoint}${baseEndpoint.includes('?') ? '&' : '?'}forceSAMLAutoRedirect=true`; await doSSOAuth(embedConfig, ssoEndPoint); return exports.loggedInStatus; }; exports.doOIDCAuth = doOIDCAuth; const logout = async (embedConfig) => { const { thoughtSpotHost } = embedConfig; await (0, authService_1.fetchLogoutService)(thoughtSpotHost); (0, resetServices_1.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; }); } exports.loggedInStatus = false; return exports.loggedInStatus; }; exports.logout = logout; /** * Perform authentication on the ThoughtSpot cluster * @param embedConfig The embed configuration */ const authenticate = async (embedConfig) => { const { authType } = embedConfig; switch (authType) { case types_1.AuthType.SSO: case types_1.AuthType.SAMLRedirect: case types_1.AuthType.SAML: return (0, exports.doSamlAuth)(embedConfig); case types_1.AuthType.OIDC: case types_1.AuthType.OIDCRedirect: return (0, exports.doOIDCAuth)(embedConfig); case types_1.AuthType.AuthServer: case types_1.AuthType.TrustedAuthToken: return (0, exports.doTokenAuth)(embedConfig); case types_1.AuthType.TrustedAuthTokenCookieless: return (0, exports.doCookielessTokenAuth)(embedConfig); case types_1.AuthType.Basic: return (0, exports.doBasicAuth)(embedConfig); default: return Promise.resolve(true); } }; exports.authenticate = authenticate; /** * Check if we are authenticated to the ThoughtSpot cluster */ const isAuthenticated = () => exports.loggedInStatus; exports.isAuthenticated = isAuthenticated; //# sourceMappingURL=auth.js.map