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