@openpass/openpass-js-sdk
Version:
OpenPass SSO JavaScript SDK
279 lines • 13.9 kB
JavaScript
;
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