UNPKG

@openpass/openpass-js-sdk

Version:
274 lines 13.9 kB
import { config, getOpenPassApiBaseUrl } from "../config"; import { PARAM_CODE_CHALLENGE_METHOD_VALUE, PARAM_CODE_RESPONSE_MODE_MESSAGE, POPUP_ADVANCE_MESSAGE_SOURCE, POPUP_MESSAGE_SOURCE, } from "./constants"; import { generateCodeChallenge, generateCodeVerifier } from "./utils/pkce"; import { generateStateValue } from "./utils/state"; import { buildAuthorizeUrl, matchesEventOrigin } from "./url"; import { isPostMessageSupported } from "./utils/browser"; import { isLargeBreakpoint, windowOpenPopupWithPercentage, windowOpenPopup } from "./utils/window"; import { ERROR_CODE_INVALID_AUTH_CODE } from "./error/codes"; import { AuthCancelledError, AuthError, SdkError } from "./error/errors"; import AbortablePromise from "./utils/abortablePromise"; import { sendSdkTelemetryErrorEvent, sendSdkTelemetryInfoEvent } from "./utils/sdkTelemetry"; import { isAuthCodeValid } from "./utils/authCode"; import { reportAnalyticsEvent, addAnalyticsDimension } from "./utils/userAnalytics"; 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. */ export default class PopupAuth { constructor(openPassOptions, redirectApi, openPassApiClient) { 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; } async signInWithPopup(options) { // Report analytics event for popup sign-in started reportAnalyticsEvent("sign-in-with-pop-up-started", { source: options.source }); // 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) { sendSdkTelemetryInfoEvent("PopupAuth.signInWithPopup", "Popup failed to open, falling back to redirect", this.openPassApiClient); this.redirectApi.signIn({ ...options, redirectUrl: options.redirectUrl, }); throw new 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 SdkError(exceptionMessage); } const authWindow = { window: popup }; this.popupWindow = authWindow; return this.doLogin(authWindow, options); } async doLogin(authWindow, options) { var _a, _b; const popupCloseHandler = () => { this.closePopupIfExists(authWindow); }; window.addEventListener("beforeunload", popupCloseHandler); // 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, getOpenPassApiBaseUrl(this.openPassOptions.baseUrl)); } }, 500); try { const verifier = 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 generateCodeChallenge(verifier), codeChallengeMethod: PARAM_CODE_CHALLENGE_METHOD_VALUE, state: generateStateValue(), responseMode: 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 = buildAuthorizeUrl(getOpenPassApiBaseUrl(this.openPassOptions.baseUrl), 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 AuthCancelledError)) { // Only send telemetry events if the error is not due to the user closing the popup sendSdkTelemetryErrorEvent("PopupAuth.doLogin", e, this.openPassApiClient); // Close the popup if an error occurs this.closePopupIfExists(authWindow); } throw e; } finally { window.removeEventListener("beforeunload", popupCloseHandler); clearInterval(pingIntervalId); } } async waitForPopupResponse(authWindow, authSession) { const authCodeResponse = await this.listenForPopupResponse(authWindow); if (!isAuthCodeValid(authCodeResponse, authSession) || !authCodeResponse.code) { const authError = new AuthError(authCodeResponse.error ? authCodeResponse.error : ERROR_CODE_INVALID_AUTH_CODE, authCodeResponse.errorDescription ? authCodeResponse.errorDescription : "Error, invalid authorization code response", authCodeResponse.errorUri ? authCodeResponse.errorUri : "", authSession.clientState); 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 SdkError("No Response received from popup")); }, SET_AUTHENTICATED_TIMEOUT_IN_MS); (async () => { try { this.closePopupIfExists(authWindow); const response = { clientState: authSession.clientState, originatingUri: authSession.originatingUri, idToken: idToken, rawIdToken: rawIdToken, accessToken: rawAccessToken, rawAccessToken: rawAccessToken, refreshToken: refreshToken, expiresIn: expiresIn, tokenType: tokenType, }; // Report analytics for successful authentication if (response.idToken) { reportAnalyticsEvent("authentication-completed", { method: "popup" }); addAnalyticsDimension("is_authenticated", "true"); addAnalyticsDimension("op_user_id", response.idToken.sub || ""); } resolve(response); } catch (e) { this.closePopupIfExists(authWindow); reject(e); } })(); }); } async listenForPopupResponse(authWindow) { let closeTimeout; let messageTimeout; let messageHandler; const popupPromise = new AbortablePromise((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); sendSdkTelemetryInfoEvent("PopupAuth.listenForPopupResponse", "Popup window closed, aborting sign-in", this.openPassApiClient); reject(new SdkError("Popup closed, authentication response not available")); } } }, 100); messageHandler = (event) => { //Check that the message is from the authorization server if (!matchesEventOrigin(event.origin, getOpenPassApiBaseUrl(this.openPassOptions.baseUrl))) { return; } sendSdkTelemetryInfoEvent("PopupAuth.listenForPopupResponse", `Popup message received. Has Data: ${!!event.data}`, this.openPassApiClient); if (!event.data) { return; } const { data } = event; if (!data.source || data.source !== POPUP_MESSAGE_SOURCE) { // cache advance auth message but don't resolve with it yet if (data.source == POPUP_ADVANCE_MESSAGE_SOURCE) { advanceAuthMessage = data; } return; } resolve(data); }; window.addEventListener("message", messageHandler, false); messageTimeout = setInterval(() => { clearInterval(messageTimeout); reject(new SdkError("No Response received from popup")); sendSdkTelemetryInfoEvent("PopupAuth.listenForPopupResponse", "No Response received from popup after maximum timeout", this.openPassApiClient); }, config.POPUP_RESPONSE_TIMEOUT_MS); onAbort(() => { clearInterval(closeTimeout); clearTimeout(messageTimeout); window.removeEventListener("message", messageHandler); reject(new AuthCancelledError("Popup window was closed")); }); }); authWindow.listener = popupPromise; return popupPromise.finally(() => { clearInterval(closeTimeout); clearTimeout(messageTimeout); window.removeEventListener("message", messageHandler); }); } openPopup() { if (!isPostMessageSupported()) { return null; } if (isLargeBreakpoint()) { return windowOpenPopup("", POPUP_NAME, POPUP_WIDTH, POPUP_HEIGHT); } else { return 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; } } } //# sourceMappingURL=popup.js.map