UNPKG

@openpass/openpass-js-sdk

Version:
279 lines 13.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const config_1 = require("../config"); const constants_1 = require("./constants"); const pkce_1 = require("./utils/pkce"); const state_1 = require("./utils/state"); const url_1 = require("./url"); const browser_1 = require("./utils/browser"); const window_1 = require("./utils/window"); const codes_1 = require("./error/codes"); const errors_1 = require("./error/errors"); const abortablePromise_1 = __importDefault(require("./utils/abortablePromise")); const sdkTelemetry_1 = require("./utils/sdkTelemetry"); const authCode_1 = require("./utils/authCode"); const POPUP_HEIGHT = 586; const POPUP_WIDTH = 428; const POPUP_NAME = "openpass:popup:login"; // Used to close the popup in the event the set authenticated method requires a remote API call to complete the authentication // process, and that call suffers from increased latency. const SET_AUTHENTICATED_TIMEOUT_IN_MS = 10000; /** * Class which handles the Popup authorization flow. * This class redirects to the authorization server and waits for a response from the authorization server. * The response is sent via the window.postMessage API to support secure cross-origin communication. */ class PopupAuth { constructor(openPassOptions, redirectApi, openPassApiClient) { this.popupCloseHandler = () => { this.closePopupIfExists(this.popupWindow); }; this.openPassOptions = openPassOptions; this.openPassApiClient = openPassApiClient; this.redirectApi = redirectApi; } refocusIfPopupExists() { if (!this.popupWindow || this.popupWindow.window.closed) { return false; } this.popupWindow.window.focus(); return true; } hasActivePopup() { return !!(this.popupWindow && !this.popupWindow.window.closed); } async signInWithPopup(options) { // If an existing popup flow is in progress, close that and start a new one this.closePopupIfExists(this.popupWindow); let popup; let popupErrorMessage = null; try { popup = this.openPopup(); } catch (error) { popup = null; popupErrorMessage = error === null || error === void 0 ? void 0 : error.message; } if (!popup) { // If the popup was blocked, and a fallback redirectUrl was supplied, try to redirect instead if (options.redirectUrl) { (0, sdkTelemetry_1.sendSdkTelemetryInfoEvent)("PopupAuth.signInWithPopup", "Popup failed to open, falling back to redirect", this.openPassApiClient); this.redirectApi.signIn({ ...options, redirectUrl: options.redirectUrl, }); throw new errors_1.SdkError("Using redirect instead of popup. This error should not be thrown because the redirect happens first."); } let exceptionMessage = "Popup window did not open correctly."; if (popupErrorMessage) { exceptionMessage += ` Error: ${popupErrorMessage}`; } throw new errors_1.SdkError(exceptionMessage); } const authWindow = { window: popup }; this.popupWindow = authWindow; return this.doLogin(authWindow, options); } async doLogin(authWindow, options) { var _a, _b; this.addPopupCloseOnBeforeUnloadHandler(); // Ping the SDK popup every 500ms to provide it with a reference to this parent window. // If Chrome discards a page to recover resources, the window.opener reference is lost in the popup. // On receiving the ping message, the popup is provided with the message.source which is this window, // which it can use to send the authentication complete message. const pingIntervalId = setInterval(() => { if (authWindow.window && !authWindow.window.closed) { const pingMessage = { source: "openpass-popup-ping", }; authWindow.window.postMessage(pingMessage, (0, config_1.getOpenPassApiBaseUrl)(this.openPassOptions.baseUrl)); } }, 500); try { const verifier = (0, pkce_1.generateCodeVerifier)(); const authSession = { clientState: options === null || options === void 0 ? void 0 : options.clientState, clientId: this.openPassOptions.clientId, redirectUrl: options === null || options === void 0 ? void 0 : options.redirectUrl, codeVerifier: verifier, codeChallenge: await (0, pkce_1.generateCodeChallenge)(verifier), codeChallengeMethod: constants_1.PARAM_CODE_CHALLENGE_METHOD_VALUE, state: (0, state_1.generateStateValue)(), responseMode: constants_1.PARAM_CODE_RESPONSE_MODE_MESSAGE, loginHint: options === null || options === void 0 ? void 0 : options.loginHint, disableLoginHintEditing: options === null || options === void 0 ? void 0 : options.disableLoginHintEditing, originatingUri: (_a = window === null || window === void 0 ? void 0 : window.location) === null || _a === void 0 ? void 0 : _a.href, allowUnverifiedEmail: (_b = options === null || options === void 0 ? void 0 : options.allowUnverifiedEmail) !== null && _b !== void 0 ? _b : false, }; const source = options ? options.source : "Custom"; const loginUri = (0, url_1.buildAuthorizeUrl)((0, config_1.getOpenPassApiBaseUrl)(this.openPassOptions.baseUrl), config_1.config.SSO_AUTHORIZE_PATH, authSession, source, options === null || options === void 0 ? void 0 : options.customQueryParameters); try { authWindow.window.location.replace(loginUri); } catch (_c) { // If the window location replace fails, try to set the href directly authWindow.window.location.href = loginUri; } return await this.waitForPopupResponse(authWindow, authSession); } catch (e) { if (!(e instanceof errors_1.AuthCancelledError)) { // Only send telemetry events if the error is not due to the user closing the popup (0, sdkTelemetry_1.sendSdkTelemetryErrorEvent)("PopupAuth.doLogin", e, this.openPassApiClient); // Close the popup if an error occurs this.closePopupIfExists(authWindow); } throw e; } finally { this.removePopupCloseOnBeforeUnloadHandler(); clearInterval(pingIntervalId); } } addPopupCloseOnBeforeUnloadHandler() { window.addEventListener("beforeunload", this.popupCloseHandler); } removePopupCloseOnBeforeUnloadHandler() { window.removeEventListener("beforeunload", this.popupCloseHandler); } async waitForPopupResponse(authWindow, authSession) { const authCodeResponse = await this.listenForPopupResponse(authWindow); if (!(0, authCode_1.isAuthCodeValid)(authCodeResponse, authSession) || !authCodeResponse.code) { const authError = new errors_1.AuthError(authCodeResponse.error ? authCodeResponse.error : codes_1.ERROR_CODE_INVALID_AUTH_CODE, authCodeResponse.errorDescription ? authCodeResponse.errorDescription : "Error, invalid authorization code response", authCodeResponse.errorUri ? authCodeResponse.errorUri : "", authSession.clientState); (0, sdkTelemetry_1.sendSdkTelemetryErrorEvent)("PopupAuth.waitForPopupResponse", authError, this.openPassApiClient); throw authError; } const openPassTokens = await this.openPassApiClient.exchangeAuthCodeForTokens(authCodeResponse.code, authSession); return await this.completeAuthentication(authWindow, openPassTokens, authSession); } async completeAuthentication(authWindow, tokens, authSession) { const { idToken, rawIdToken, rawAccessToken, refreshToken, expiresIn, tokenType } = tokens; return new Promise((resolve, reject) => { //wait for identity to be set before closing the popup setTimeout(() => { reject(new errors_1.SdkError("No Response received from popup")); }, SET_AUTHENTICATED_TIMEOUT_IN_MS); (async () => { try { this.closePopupIfExists(authWindow); resolve({ clientState: authSession.clientState, originatingUri: authSession.originatingUri, idToken: idToken, rawIdToken: rawIdToken, accessToken: rawAccessToken, rawAccessToken: rawAccessToken, refreshToken: refreshToken, expiresIn: expiresIn, tokenType: tokenType, }); } catch (e) { this.closePopupIfExists(authWindow); reject(e); } })(); }); } async listenForPopupResponse(authWindow) { let closeTimeout; let messageTimeout; let messageHandler; const popupPromise = new abortablePromise_1.default((resolve, reject, onAbort) => { let advanceAuthMessage = null; closeTimeout = setInterval(() => { if (authWindow.window && authWindow.window.closed) { if (advanceAuthMessage) { resolve(advanceAuthMessage); } else { clearInterval(closeTimeout); window.removeEventListener("message", messageHandler); this.closePopupIfExists(authWindow); (0, sdkTelemetry_1.sendSdkTelemetryInfoEvent)("PopupAuth.listenForPopupResponse", "Popup window closed, aborting sign-in", this.openPassApiClient); reject(new errors_1.SdkError("Popup closed, authentication response not available")); } } }, 100); messageHandler = (event) => { //Check that the message is from the authorization server if (!(0, url_1.matchesEventOrigin)(event.origin, (0, config_1.getOpenPassApiBaseUrl)(this.openPassOptions.baseUrl))) { return; } (0, sdkTelemetry_1.sendSdkTelemetryInfoEvent)("PopupAuth.listenForPopupResponse", `Popup message received. Has Data: ${!!event.data}`, this.openPassApiClient); if (!event.data) { return; } const { data } = event; if (!data.source || data.source !== constants_1.POPUP_MESSAGE_SOURCE) { // cache advance auth message but don't resolve with it yet if (data.source == constants_1.POPUP_ADVANCE_MESSAGE_SOURCE) { advanceAuthMessage = data; } return; } resolve(data); }; window.addEventListener("message", messageHandler, false); messageTimeout = setInterval(() => { clearInterval(messageTimeout); reject(new errors_1.SdkError("No Response received from popup")); (0, sdkTelemetry_1.sendSdkTelemetryInfoEvent)("PopupAuth.listenForPopupResponse", "No Response received from popup after maximum timeout", this.openPassApiClient); }, config_1.config.POPUP_RESPONSE_TIMEOUT_MS); onAbort(() => { clearInterval(closeTimeout); clearTimeout(messageTimeout); window.removeEventListener("message", messageHandler); reject(new errors_1.AuthCancelledError("Popup window was closed")); }); }); authWindow.listener = popupPromise; return popupPromise.finally(() => { clearInterval(closeTimeout); clearTimeout(messageTimeout); window.removeEventListener("message", messageHandler); }); } openPopup() { if (!(0, browser_1.isPostMessageSupported)()) { return null; } if ((0, window_1.isLargeBreakpoint)()) { return (0, window_1.windowOpenPopup)("", POPUP_NAME, POPUP_WIDTH, POPUP_HEIGHT); } else { return (0, window_1.windowOpenPopupWithPercentage)("", POPUP_NAME, 100); } } closePopupIfExists(popupWindowToClose) { if (!popupWindowToClose) { return; } if (popupWindowToClose.window && !popupWindowToClose.window.closed) { try { popupWindowToClose.window.close(); } catch (e) { console.warn("Error closing the openpass popup window", e); } } if (popupWindowToClose.listener) { try { popupWindowToClose.listener.abort(); popupWindowToClose.listener = undefined; } catch (e) { console.warn("Error aborting the openpass popup listener", e); } } // Only set the popup to null if the closed window was the current popup if (popupWindowToClose == this.popupWindow) { this.popupWindow = undefined; } } } exports.default = PopupAuth; //# sourceMappingURL=popup.js.map