UNPKG

@openpass/openpass-js-sdk

Version:
144 lines 8.1 kB
import { config, getOpenPassApiBaseUrl } from "../config"; import { PARAM_CODE_CHALLENGE_METHOD_VALUE, PARAM_CODE_RESPONSE_MODE_MESSAGE, IFRAME_MESSAGE_SOURCE, SILENT_AUTH_OUTGOING_MESSAGE_TYPE, } from "./constants"; import { generateCodeChallenge, generateCodeVerifier } from "./utils/pkce"; import { generateStateValue } from "./utils/state"; import { buildAuthorizeUrl, matchesEventOrigin } from "./url"; import { ERROR_CODE_INVALID_AUTH_CODE } from "./error/codes"; import { sendSdkTelemetryErrorEvent, sendSdkTelemetryInfoEvent } from "./utils/sdkTelemetry"; import { AuthError, SdkError } from "./error/errors"; import { isAuthCodeValid } from "./utils/authCode"; import { reportAnalyticsEvent, addAnalyticsDimension } from "./utils/userAnalytics"; /* * Handle silent authentication for QuickAuth and InlineSignIn popup mode */ export default class SilentAuth { constructor(openPassOptions, openPassApiClient, popup) { this.popup = popup; this.openPassOptions = openPassOptions; this.openPassApiClient = openPassApiClient; } async handleSilentAuthWithPopupFallback(parentContainer, options, openPassApiBaseUrl, iframe, popupFallbackFlow) { var _a; let authResponse; try { authResponse = await this.silentAuth(parentContainer, options); } catch (error) { (_a = iframe === null || iframe === void 0 ? void 0 : iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage({ type: SILENT_AUTH_OUTGOING_MESSAGE_TYPE.SilentAuthSignInCompletion, }, openPassApiBaseUrl); //if silent auth fails, then popup fails and auth starts again - need to refocus if (this.popup.refocusIfPopupExists()) { return; } //Inline sign in uses its own handlePopupSignIn method authResponse = popupFallbackFlow ? await popupFallbackFlow() : await this.popup.signInWithPopup(options); } return authResponse; } async silentAuth(parentContainer, options, openPassApiBaseUrl) { var _a; const authIframe = this.createHiddenSilentAuthIframe(parentContainer); 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, useSilentAuth: true, allowUnverifiedEmail: options.allowUnverifiedEmail, }; 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); authIframe.src = loginUri; reportAnalyticsEvent("silent-auth-started", { source }); return await this.waitForIframeResponse(authIframe, authSession, parentContainer, options); } async waitForIframeResponse(iframe, authSession, parentContainer, options, openPassApiBaseUrl) { const authCodeResponse = await this.listenForIframeResponse(iframe); reportAnalyticsEvent("silent-auth-iframe-response-received"); 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("SilentAuth:WaitForIframeResponse", authError, this.openPassApiClient); parentContainer.removeChild(iframe); throw authError; } addAnalyticsDimension("is_op_session_detected", "true"); const openPassTokens = await this.openPassApiClient.exchangeAuthCodeForTokens(authCodeResponse.code, authSession); try { const { idToken, rawIdToken, rawAccessToken, refreshToken, expiresIn, tokenType } = openPassTokens; addAnalyticsDimension("is_authenticated", "true"); addAnalyticsDimension("op_user_id", idToken.sub || ""); return { clientState: authSession.clientState, originatingUri: authSession.originatingUri, idToken: idToken, rawIdToken: rawIdToken, accessToken: rawAccessToken, rawAccessToken: rawAccessToken, refreshToken: refreshToken, expiresIn: expiresIn, tokenType: tokenType, }; } catch (e) { throw new SdkError("Error retrieving tokens from iframe response: " + e); } finally { parentContainer.removeChild(iframe); } } createHiddenSilentAuthIframe(parentContainer) { const quickAuthIFrame = document.createElement("iframe"); quickAuthIFrame.style.setProperty("display", "none", "important"); if (!parentContainer) { throw new SdkError("Parent container is not defined: unable to create hidden iframe"); } parentContainer.appendChild(quickAuthIFrame); return quickAuthIFrame; } async listenForIframeResponse(iframe) { let messageTimeout; let messageHandler; // We create a promise that will resolve with the authentication data once the iframe posts the message const iframePromise = new Promise((resolve, reject) => { messageHandler = (event) => { // Check that the message is from the authorization server if (!matchesEventOrigin(event.origin, getOpenPassApiBaseUrl(this.openPassOptions.baseUrl))) { return; } sendSdkTelemetryInfoEvent("IframeAuth.listenForIframeResponse", `Iframe message received. Has Data: ${!!event.data}`, this.openPassApiClient); if (!event.data) { return; } const { data } = event; // Ensure the message source is from the expected iframe source if (!data.source || data.source !== IFRAME_MESSAGE_SOURCE) { console.warn("Received message from unexpected source:", data.source); return; } resolve(data); }; // Listen for messages from the iframe window.addEventListener("message", messageHandler, false); // Set up timeout in case no message is received from the iframe messageTimeout = window.setTimeout(() => { reject(new SdkError("No Response received from iframe")); sendSdkTelemetryInfoEvent("IframeAuth.listenForIframeResponse", "No Response received from iframe after maximum timeout", this.openPassApiClient); }, config.IFRAME_RESPONSE_TIMEOUT_MS); }); // Return the promise and clean up return iframePromise.finally(() => { clearTimeout(messageTimeout); window.removeEventListener("message", messageHandler); }); } } //# sourceMappingURL=silentAuth.js.map