/*
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
* Licensed under the MIT License.
|
*/
|
|
import {
|
AuthorizationCodeClient,
|
CommonEndSessionRequest,
|
UrlString,
|
AuthError,
|
IPerformanceClient,
|
Logger,
|
ICrypto,
|
ProtocolMode,
|
PerformanceEvents,
|
Constants,
|
invokeAsync,
|
invoke,
|
PkceCodes,
|
CommonAuthorizationUrlRequest,
|
ProtocolUtils,
|
} from "@azure/msal-common/browser";
|
import {
|
initializeAuthorizationRequest,
|
StandardInteractionClient,
|
} from "./StandardInteractionClient.js";
|
import * as BrowserPerformanceEvents from "../telemetry/BrowserPerformanceEvents.js";
|
import { EventType } from "../event/EventType.js";
|
import {
|
InteractionType,
|
ApiId,
|
BrowserConstants,
|
} from "../utils/BrowserConstants.js";
|
import { EndSessionPopupRequest } from "../request/EndSessionPopupRequest.js";
|
import { NavigationOptions } from "../navigation/NavigationOptions.js";
|
import * as BrowserUtils from "../utils/BrowserUtils.js";
|
import { PopupRequest } from "../request/PopupRequest.js";
|
import {
|
createBrowserAuthError,
|
BrowserAuthErrorCodes,
|
} from "../error/BrowserAuthError.js";
|
import { INavigationClient } from "../navigation/INavigationClient.js";
|
import { EventHandler } from "../event/EventHandler.js";
|
import { BrowserCacheManager } from "../cache/BrowserCacheManager.js";
|
import { BrowserConfiguration } from "../config/Configuration.js";
|
import { PopupWindowAttributes } from "../request/PopupWindowAttributes.js";
|
import { EventError } from "../event/EventMessage.js";
|
import { AuthenticationResult } from "../response/AuthenticationResult.js";
|
import * as ResponseHandler from "../response/ResponseHandler.js";
|
import * as Authorize from "../protocol/Authorize.js";
|
import { generatePkceCodes } from "../crypto/PkceGenerator.js";
|
import { isPlatformAuthAllowed } from "../broker/nativeBroker/PlatformAuthProvider.js";
|
import { generateEarKey } from "../crypto/BrowserCrypto.js";
|
import { IPlatformAuthHandler } from "../broker/nativeBroker/IPlatformAuthHandler.js";
|
import {
|
clearCacheOnLogout,
|
getDiscoveredAuthority,
|
initializeServerTelemetryManager,
|
} from "./BaseInteractionClient.js";
|
import { validateRequestMethod } from "../request/RequestHelpers.js";
|
|
export type PopupParams = {
|
popup?: Window | null;
|
popupName: string;
|
popupWindowAttributes: PopupWindowAttributes;
|
popupWindowParent: Window;
|
};
|
|
export class PopupClient extends StandardInteractionClient {
|
private currentWindow: Window | undefined;
|
protected nativeStorage: BrowserCacheManager;
|
|
constructor(
|
config: BrowserConfiguration,
|
storageImpl: BrowserCacheManager,
|
browserCrypto: ICrypto,
|
logger: Logger,
|
eventHandler: EventHandler,
|
navigationClient: INavigationClient,
|
performanceClient: IPerformanceClient,
|
nativeStorageImpl: BrowserCacheManager,
|
correlationId: string,
|
platformAuthHandler?: IPlatformAuthHandler
|
) {
|
super(
|
config,
|
storageImpl,
|
browserCrypto,
|
logger,
|
eventHandler,
|
navigationClient,
|
performanceClient,
|
correlationId,
|
platformAuthHandler
|
);
|
this.nativeStorage = nativeStorageImpl;
|
this.eventHandler = eventHandler;
|
}
|
|
/**
|
* Acquires tokens by opening a popup window to the /authorize endpoint of the authority
|
* @param request
|
* @param pkceCodes
|
*/
|
acquireToken(
|
request: PopupRequest,
|
pkceCodes?: PkceCodes
|
): Promise<AuthenticationResult> {
|
let popupParams: PopupParams | undefined = undefined;
|
try {
|
const popupName = this.generatePopupName(
|
request.scopes || Constants.OIDC_DEFAULT_SCOPES,
|
request.authority || this.config.auth.authority
|
);
|
popupParams = {
|
popupName,
|
popupWindowAttributes: request.popupWindowAttributes || {},
|
popupWindowParent: request.popupWindowParent ?? window,
|
};
|
|
this.performanceClient.addFields(
|
{ isAsyncPopup: !this.config.system.navigatePopups },
|
this.correlationId
|
);
|
|
// navigatePopups flag is false. Acquires token without first opening popup. Popup will be opened later asynchronously.
|
if (!this.config.system.navigatePopups) {
|
this.logger.verbose(
|
"navigatePopups set to false, acquiring token",
|
this.correlationId
|
);
|
// Passes on popup position and dimensions if in request
|
return this.acquireTokenPopupAsync(
|
request,
|
popupParams,
|
pkceCodes
|
);
|
} else {
|
// navigatePopups flag is set to true. Opens popup before acquiring token.
|
|
// Pre-validate request method to avoid opening popup if the request is invalid
|
const validatedRequest: PopupRequest = {
|
...request,
|
httpMethod: validateRequestMethod(
|
request,
|
this.config.system.protocolMode
|
),
|
};
|
|
this.logger.verbose(
|
"navigatePopups set to true, opening popup before acquiring token",
|
this.correlationId
|
);
|
popupParams.popup = this.openSizedPopup(
|
"about:blank",
|
popupParams
|
);
|
return this.acquireTokenPopupAsync(
|
validatedRequest,
|
popupParams,
|
pkceCodes
|
);
|
}
|
} catch (e) {
|
return Promise.reject(e);
|
}
|
}
|
|
/**
|
* Clears local cache for the current user then opens a popup window prompting the user to sign-out of the server
|
* @param logoutRequest
|
*/
|
logout(logoutRequest?: EndSessionPopupRequest): Promise<void> {
|
try {
|
this.logger.verbose("logoutPopup called", this.correlationId);
|
const validLogoutRequest =
|
this.initializeLogoutRequest(logoutRequest);
|
const popupParams: PopupParams = {
|
popupName: this.generateLogoutPopupName(validLogoutRequest),
|
popupWindowAttributes:
|
logoutRequest?.popupWindowAttributes || {},
|
popupWindowParent: logoutRequest?.popupWindowParent ?? window,
|
};
|
const authority = logoutRequest && logoutRequest.authority;
|
const mainWindowRedirectUri =
|
logoutRequest && logoutRequest.mainWindowRedirectUri;
|
|
// navigatePopups flag set to false. Acquires token without first opening popup. Popup will be opened later asynchronously.
|
if (!this.config.system.navigatePopups) {
|
this.logger.verbose(
|
"navigatePopups set to false",
|
this.correlationId
|
);
|
// Passes on popup position and dimensions if in request
|
return this.logoutPopupAsync(
|
validLogoutRequest,
|
popupParams,
|
authority,
|
mainWindowRedirectUri
|
);
|
} else {
|
// navigatePopups flag is set to true. Opens popup before logging out.
|
this.logger.verbose(
|
"navigatePopups set to true, opening popup",
|
this.correlationId
|
);
|
popupParams.popup = this.openSizedPopup(
|
"about:blank",
|
popupParams
|
);
|
return this.logoutPopupAsync(
|
validLogoutRequest,
|
popupParams,
|
authority,
|
mainWindowRedirectUri
|
);
|
}
|
} catch (e) {
|
// Since this function is synchronous we need to reject
|
return Promise.reject(e);
|
}
|
}
|
|
/**
|
* Helper which obtains an access_token for your API via opening a popup window in the user's browser
|
* @param request
|
* @param popupParams
|
* @param pkceCodes
|
*
|
* @returns A promise that is fulfilled when this function has completed, or rejected if an error was raised.
|
*/
|
protected async acquireTokenPopupAsync(
|
request: PopupRequest,
|
popupParams: PopupParams,
|
pkceCodes?: PkceCodes
|
): Promise<AuthenticationResult> {
|
this.logger.verbose(
|
"acquireTokenPopupAsync called",
|
this.correlationId
|
);
|
|
const validRequest = await invokeAsync(
|
initializeAuthorizationRequest,
|
BrowserPerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest,
|
this.logger,
|
this.performanceClient,
|
this.correlationId
|
)(
|
request,
|
InteractionType.Popup,
|
this.config,
|
this.browserCrypto,
|
this.browserStorage,
|
this.logger,
|
this.performanceClient,
|
this.correlationId
|
);
|
|
/*
|
* Skip pre-connect for async popups to reduce time between user interaction and popup window creation to avoid
|
* popup from being blocked by browsers with shorter popup timers
|
*/
|
if (popupParams.popup) {
|
BrowserUtils.preconnect(validRequest.authority);
|
}
|
|
const isPlatformBroker = isPlatformAuthAllowed(
|
this.config,
|
this.logger,
|
this.correlationId,
|
this.platformAuthProvider,
|
request.authenticationScheme
|
);
|
validRequest.platformBroker = isPlatformBroker;
|
|
if (this.config.system.protocolMode === ProtocolMode.EAR) {
|
return this.executeEarFlow(validRequest, popupParams, pkceCodes);
|
} else {
|
return this.executeCodeFlow(validRequest, popupParams, pkceCodes);
|
}
|
}
|
|
/**
|
* Executes auth code + PKCE flow
|
* @param request
|
* @param popupParams
|
* @param pkceCodes
|
* @returns
|
*/
|
async executeCodeFlow(
|
request: CommonAuthorizationUrlRequest,
|
popupParams: PopupParams,
|
pkceCodes?: PkceCodes
|
): Promise<AuthenticationResult> {
|
const correlationId = request.correlationId;
|
const serverTelemetryManager = initializeServerTelemetryManager(
|
ApiId.acquireTokenPopup,
|
this.config.auth.clientId,
|
this.correlationId,
|
this.browserStorage,
|
this.logger
|
);
|
|
const pkce =
|
pkceCodes ||
|
(await invokeAsync(
|
generatePkceCodes,
|
BrowserPerformanceEvents.GeneratePkceCodes,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(this.performanceClient, this.logger, correlationId));
|
|
const popupRequest = {
|
...request,
|
codeChallenge: pkce.challenge,
|
};
|
|
try {
|
// Initialize the client
|
const authClient: AuthorizationCodeClient = await invokeAsync(
|
this.createAuthCodeClient.bind(this),
|
BrowserPerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)({
|
serverTelemetryManager,
|
requestAuthority: popupRequest.authority,
|
requestAzureCloudOptions: popupRequest.azureCloudOptions,
|
requestExtraQueryParameters: popupRequest.extraQueryParameters,
|
account: popupRequest.account,
|
});
|
|
if (popupRequest.httpMethod === Constants.HttpMethod.POST) {
|
return await this.executeCodeFlowWithPost(
|
popupRequest,
|
popupParams,
|
authClient,
|
pkce.verifier
|
);
|
} else {
|
// Create acquire token url.
|
const navigateUrl = await invokeAsync(
|
Authorize.getAuthCodeRequestUrl,
|
PerformanceEvents.GetAuthCodeUrl,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
this.config,
|
authClient.authority,
|
popupRequest,
|
this.logger,
|
this.performanceClient
|
);
|
|
// Show the UI once the url has been created. Get the window handle for the popup.
|
const popupWindow: Window = this.initiateAuthRequest(
|
navigateUrl,
|
popupParams
|
);
|
this.eventHandler.emitEvent(
|
EventType.POPUP_OPENED,
|
correlationId,
|
InteractionType.Popup,
|
{ popupWindow },
|
null
|
);
|
|
// Wait for the redirect bridge response
|
const responseString = await BrowserUtils.waitForBridgeResponse(
|
this.config.system.popupBridgeTimeout,
|
this.logger,
|
this.browserCrypto,
|
request,
|
this.performanceClient
|
);
|
|
const serverParams = invoke(
|
ResponseHandler.deserializeResponse,
|
BrowserPerformanceEvents.DeserializeResponse,
|
this.logger,
|
this.performanceClient,
|
this.correlationId
|
)(
|
responseString,
|
this.config.auth.OIDCOptions.responseMode,
|
this.logger,
|
this.correlationId
|
);
|
|
return await invokeAsync(
|
Authorize.handleResponseCode,
|
BrowserPerformanceEvents.HandleResponseCode,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
request,
|
serverParams,
|
pkce.verifier,
|
ApiId.acquireTokenPopup,
|
this.config,
|
authClient,
|
this.browserStorage,
|
this.nativeStorage,
|
this.eventHandler,
|
this.logger,
|
this.performanceClient,
|
this.platformAuthProvider
|
);
|
}
|
} catch (e) {
|
// Close the synchronous popup if an error is thrown before the window unload event is registered
|
popupParams.popup?.close();
|
|
if (e instanceof AuthError) {
|
(e as AuthError).setCorrelationId(this.correlationId);
|
serverTelemetryManager.cacheFailedRequest(e);
|
}
|
throw e;
|
}
|
}
|
|
/**
|
* Executes EAR flow
|
* @param request
|
*/
|
async executeEarFlow(
|
request: CommonAuthorizationUrlRequest,
|
popupParams: PopupParams,
|
pkceCodes?: PkceCodes
|
): Promise<AuthenticationResult> {
|
const {
|
correlationId,
|
authority,
|
azureCloudOptions,
|
extraQueryParameters,
|
account,
|
} = request;
|
// Get the frame handle for the silent request
|
const discoveredAuthority = await invokeAsync(
|
getDiscoveredAuthority,
|
BrowserPerformanceEvents.StandardInteractionClientGetDiscoveredAuthority,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
this.config,
|
this.correlationId,
|
this.performanceClient,
|
this.browserStorage,
|
this.logger,
|
authority,
|
azureCloudOptions,
|
extraQueryParameters,
|
account
|
);
|
|
const earJwk = await invokeAsync(
|
generateEarKey,
|
BrowserPerformanceEvents.GenerateEarKey,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)();
|
const pkce =
|
pkceCodes ||
|
(await invokeAsync(
|
generatePkceCodes,
|
BrowserPerformanceEvents.GeneratePkceCodes,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(this.performanceClient, this.logger, correlationId));
|
const popupRequest = {
|
...request,
|
earJwk: earJwk,
|
codeChallenge: pkce.challenge,
|
};
|
const popupWindow =
|
popupParams.popup || this.openPopup("about:blank", popupParams);
|
|
const form = await Authorize.getEARForm(
|
popupWindow.document,
|
this.config,
|
discoveredAuthority,
|
popupRequest,
|
this.logger,
|
this.performanceClient
|
);
|
form.submit();
|
|
// Monitor the popup for the hash. Return the string value and close the popup when the hash is received. Default timeout is 60 seconds.
|
const responseString = await invokeAsync(
|
BrowserUtils.waitForBridgeResponse,
|
BrowserPerformanceEvents.SilentHandlerMonitorIframeForHash,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
this.config.system.popupBridgeTimeout,
|
this.logger,
|
this.browserCrypto,
|
popupRequest,
|
this.performanceClient
|
);
|
|
const serverParams = invoke(
|
ResponseHandler.deserializeResponse,
|
BrowserPerformanceEvents.DeserializeResponse,
|
this.logger,
|
this.performanceClient,
|
this.correlationId
|
)(
|
responseString,
|
this.config.auth.OIDCOptions.responseMode,
|
this.logger,
|
this.correlationId
|
);
|
|
if (!serverParams.ear_jwe && serverParams.code) {
|
const authClient = await invokeAsync(
|
this.createAuthCodeClient.bind(this),
|
BrowserPerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)({
|
serverTelemetryManager: initializeServerTelemetryManager(
|
ApiId.acquireTokenPopup,
|
this.config.auth.clientId,
|
correlationId,
|
this.browserStorage,
|
this.logger
|
),
|
requestAuthority: request.authority,
|
requestAzureCloudOptions: request.azureCloudOptions,
|
requestExtraQueryParameters: request.extraQueryParameters,
|
account: request.account,
|
authority: discoveredAuthority,
|
});
|
|
return invokeAsync(
|
Authorize.handleResponseCode,
|
BrowserPerformanceEvents.HandleResponseCode,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
popupRequest,
|
serverParams,
|
pkce.verifier,
|
ApiId.acquireTokenPopup,
|
this.config,
|
authClient,
|
this.browserStorage,
|
this.nativeStorage,
|
this.eventHandler,
|
this.logger,
|
this.performanceClient,
|
this.platformAuthProvider
|
);
|
} else {
|
return invokeAsync(
|
Authorize.handleResponseEAR,
|
BrowserPerformanceEvents.HandleResponseEar,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
popupRequest,
|
serverParams,
|
ApiId.acquireTokenPopup,
|
this.config,
|
discoveredAuthority,
|
this.browserStorage,
|
this.nativeStorage,
|
this.eventHandler,
|
this.logger,
|
this.performanceClient,
|
this.platformAuthProvider
|
);
|
}
|
}
|
|
async executeCodeFlowWithPost(
|
request: CommonAuthorizationUrlRequest,
|
popupParams: PopupParams,
|
authClient: AuthorizationCodeClient,
|
pkceVerifier: string
|
): Promise<AuthenticationResult> {
|
const correlationId = request.correlationId;
|
// Get the frame handle for the silent request
|
const discoveredAuthority = await invokeAsync(
|
getDiscoveredAuthority,
|
BrowserPerformanceEvents.StandardInteractionClientGetDiscoveredAuthority,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
this.config,
|
this.correlationId,
|
this.performanceClient,
|
this.browserStorage,
|
this.logger
|
);
|
|
const popupWindow =
|
popupParams.popup || this.openPopup("about:blank", popupParams);
|
|
const form = await Authorize.getCodeForm(
|
popupWindow.document,
|
this.config,
|
discoveredAuthority,
|
request,
|
this.logger,
|
this.performanceClient
|
);
|
|
form.submit();
|
|
// Monitor the popup for the hash. Return the string value and close the popup when the hash is received. Default timeout is 60 seconds.
|
const responseString = await invokeAsync(
|
BrowserUtils.waitForBridgeResponse,
|
BrowserPerformanceEvents.SilentHandlerMonitorIframeForHash,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
this.config.system.popupBridgeTimeout,
|
this.logger,
|
this.browserCrypto,
|
request,
|
this.performanceClient
|
);
|
|
const serverParams = invoke(
|
ResponseHandler.deserializeResponse,
|
BrowserPerformanceEvents.DeserializeResponse,
|
this.logger,
|
this.performanceClient,
|
this.correlationId
|
)(
|
responseString,
|
this.config.auth.OIDCOptions.responseMode,
|
this.logger,
|
this.correlationId
|
);
|
|
return invokeAsync(
|
Authorize.handleResponseCode,
|
BrowserPerformanceEvents.HandleResponseCode,
|
this.logger,
|
this.performanceClient,
|
correlationId
|
)(
|
request,
|
serverParams,
|
pkceVerifier,
|
ApiId.acquireTokenPopup,
|
this.config,
|
authClient,
|
this.browserStorage,
|
this.nativeStorage,
|
this.eventHandler,
|
this.logger,
|
this.performanceClient,
|
this.platformAuthProvider
|
);
|
}
|
|
/**
|
*
|
* @param validRequest
|
* @param popupName
|
* @param requestAuthority
|
* @param popup
|
* @param mainWindowRedirectUri
|
* @param popupWindowAttributes
|
*/
|
protected async logoutPopupAsync(
|
validRequest: CommonEndSessionRequest,
|
popupParams: PopupParams,
|
requestAuthority?: string,
|
mainWindowRedirectUri?: string
|
): Promise<void> {
|
this.logger.verbose("logoutPopupAsync called", this.correlationId);
|
this.eventHandler.emitEvent(
|
EventType.LOGOUT_START,
|
this.correlationId,
|
InteractionType.Popup,
|
validRequest
|
);
|
|
const serverTelemetryManager = initializeServerTelemetryManager(
|
ApiId.logoutPopup,
|
this.config.auth.clientId,
|
this.correlationId,
|
this.browserStorage,
|
this.logger
|
);
|
|
try {
|
// Clear cache on logout
|
await clearCacheOnLogout(
|
this.browserStorage,
|
this.browserCrypto,
|
this.logger,
|
this.correlationId,
|
validRequest.account
|
);
|
|
// Initialize the client
|
const authClient = await invokeAsync(
|
this.createAuthCodeClient.bind(this),
|
BrowserPerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
|
this.logger,
|
this.performanceClient,
|
this.correlationId
|
)({
|
serverTelemetryManager,
|
requestAuthority: requestAuthority,
|
account: validRequest.account || undefined,
|
});
|
|
try {
|
authClient.authority.endSessionEndpoint;
|
} catch {
|
if (
|
validRequest.account?.homeAccountId &&
|
validRequest.postLogoutRedirectUri &&
|
authClient.authority.protocolMode === ProtocolMode.OIDC
|
) {
|
this.eventHandler.emitEvent(
|
EventType.LOGOUT_SUCCESS,
|
validRequest.correlationId,
|
InteractionType.Popup,
|
validRequest
|
);
|
|
if (mainWindowRedirectUri) {
|
const navigationOptions: NavigationOptions = {
|
apiId: ApiId.logoutPopup,
|
timeout:
|
this.config.system.redirectNavigationTimeout,
|
noHistory: false,
|
};
|
const absoluteUrl = UrlString.getAbsoluteUrl(
|
mainWindowRedirectUri,
|
BrowserUtils.getCurrentUri()
|
);
|
await this.navigationClient.navigateInternal(
|
absoluteUrl,
|
navigationOptions
|
);
|
}
|
|
popupParams.popup?.close();
|
|
return;
|
}
|
}
|
|
// Redirect bridge requires "state" param to work properly
|
validRequest.state = ProtocolUtils.setRequestState(
|
this.browserCrypto,
|
validRequest.state || "",
|
{
|
interactionType: InteractionType.Popup,
|
}
|
);
|
|
// Create logout string and navigate user window to logout.
|
const logoutUri: string = authClient.getLogoutUri(validRequest);
|
|
this.eventHandler.emitEvent(
|
EventType.LOGOUT_SUCCESS,
|
validRequest.correlationId,
|
InteractionType.Popup,
|
validRequest
|
);
|
|
// Open the popup window to requestUrl.
|
const popupWindow = this.openPopup(logoutUri, popupParams);
|
this.eventHandler.emitEvent(
|
EventType.POPUP_OPENED,
|
validRequest.correlationId,
|
InteractionType.Popup,
|
{ popupWindow },
|
null
|
);
|
|
await BrowserUtils.waitForBridgeResponse(
|
this.config.system.popupBridgeTimeout,
|
this.logger,
|
this.browserCrypto,
|
validRequest,
|
this.performanceClient
|
).catch(() => {
|
// Swallow any errors related to monitoring the window. Server logout is best effort
|
});
|
|
if (mainWindowRedirectUri) {
|
const navigationOptions: NavigationOptions = {
|
apiId: ApiId.logoutPopup,
|
timeout: this.config.system.redirectNavigationTimeout,
|
noHistory: false,
|
};
|
const absoluteUrl = UrlString.getAbsoluteUrl(
|
mainWindowRedirectUri,
|
BrowserUtils.getCurrentUri()
|
);
|
|
this.logger.verbose(
|
"Redirecting main window to url specified in the request",
|
this.correlationId
|
);
|
this.logger.verbosePii(
|
`Redirecting main window to: '${absoluteUrl}'`,
|
this.correlationId
|
);
|
await this.navigationClient.navigateInternal(
|
absoluteUrl,
|
navigationOptions
|
);
|
} else {
|
this.logger.verbose(
|
"No main window navigation requested",
|
this.correlationId
|
);
|
}
|
} catch (e) {
|
// Close the synchronous popup if an error is thrown before the window unload event is registered
|
popupParams.popup?.close();
|
|
if (e instanceof AuthError) {
|
(e as AuthError).setCorrelationId(this.correlationId);
|
serverTelemetryManager.cacheFailedRequest(e);
|
}
|
this.eventHandler.emitEvent(
|
EventType.LOGOUT_FAILURE,
|
this.correlationId,
|
InteractionType.Popup,
|
null,
|
e as EventError
|
);
|
this.eventHandler.emitEvent(
|
EventType.LOGOUT_END,
|
this.correlationId,
|
InteractionType.Popup
|
);
|
throw e;
|
}
|
|
this.eventHandler.emitEvent(
|
EventType.LOGOUT_END,
|
this.correlationId,
|
InteractionType.Popup
|
);
|
}
|
|
/**
|
* Opens a popup window with given request Url.
|
* @param requestUrl
|
*/
|
initiateAuthRequest(requestUrl: string, params: PopupParams): Window {
|
// Check that request url is not empty.
|
if (requestUrl) {
|
this.logger.infoPii(
|
`Navigate to: '${requestUrl}'`,
|
this.correlationId
|
);
|
// Open the popup window to requestUrl.
|
return this.openPopup(requestUrl, params);
|
} else {
|
// Throw error if request URL is empty.
|
this.logger.error("Navigate url is empty", this.correlationId);
|
throw createBrowserAuthError(
|
BrowserAuthErrorCodes.emptyNavigateUri
|
);
|
}
|
}
|
|
/**
|
* @hidden
|
*
|
* Configures popup window for login.
|
*
|
* @param urlNavigate
|
* @param title
|
* @param popUpWidth
|
* @param popUpHeight
|
* @param popupWindowAttributes
|
* @ignore
|
* @hidden
|
*/
|
openPopup(urlNavigate: string, popupParams: PopupParams): Window {
|
try {
|
let popupWindow;
|
// Popup window passed in, setting url to navigate to
|
if (popupParams.popup) {
|
popupWindow = popupParams.popup;
|
this.logger.verbosePii(
|
`Navigating popup window to: '${urlNavigate}'`,
|
this.correlationId
|
);
|
popupWindow.location.assign(urlNavigate);
|
} else if (typeof popupParams.popup === "undefined") {
|
// Popup will be undefined if it was not passed in
|
this.logger.verbosePii(
|
`Opening popup window to: '${urlNavigate}'`,
|
this.correlationId
|
);
|
popupWindow = this.openSizedPopup(urlNavigate, popupParams);
|
}
|
|
// Popup will be null if popups are blocked
|
if (!popupWindow) {
|
throw createBrowserAuthError(
|
BrowserAuthErrorCodes.emptyWindowError
|
);
|
}
|
if (popupWindow.focus) {
|
popupWindow.focus();
|
}
|
this.currentWindow = popupWindow;
|
|
return popupWindow;
|
} catch (e) {
|
this.logger.error(
|
`error opening popup '${(e as AuthError).message}'`,
|
this.correlationId
|
);
|
throw createBrowserAuthError(
|
BrowserAuthErrorCodes.popupWindowError
|
);
|
}
|
}
|
|
/**
|
* Helper function to set popup window dimensions and position
|
* @param urlNavigate
|
* @param popupName
|
* @param popupWindowAttributes
|
* @returns
|
*/
|
openSizedPopup(
|
urlNavigate: string,
|
{ popupName, popupWindowAttributes, popupWindowParent }: PopupParams
|
): Window | null {
|
/**
|
* adding winLeft and winTop to account for dual monitor
|
* using screenLeft and screenTop for IE8 and earlier
|
*/
|
const winLeft = popupWindowParent.screenLeft
|
? popupWindowParent.screenLeft
|
: popupWindowParent.screenX;
|
const winTop = popupWindowParent.screenTop
|
? popupWindowParent.screenTop
|
: popupWindowParent.screenY;
|
/**
|
* window.innerWidth displays browser window"s height and width excluding toolbars
|
* using document.documentElement.clientWidth for IE8 and earlier
|
*/
|
const winWidth =
|
popupWindowParent.innerWidth ||
|
document.documentElement.clientWidth ||
|
document.body.clientWidth;
|
const winHeight =
|
popupWindowParent.innerHeight ||
|
document.documentElement.clientHeight ||
|
document.body.clientHeight;
|
|
let width = popupWindowAttributes.popupSize?.width;
|
let height = popupWindowAttributes.popupSize?.height;
|
let top = popupWindowAttributes.popupPosition?.top;
|
let left = popupWindowAttributes.popupPosition?.left;
|
|
if (!width || width < 0 || width > winWidth) {
|
this.logger.verbose(
|
"Default popup window width used. Window width not configured or invalid.",
|
this.correlationId
|
);
|
width = BrowserConstants.POPUP_WIDTH;
|
}
|
|
if (!height || height < 0 || height > winHeight) {
|
this.logger.verbose(
|
"Default popup window height used. Window height not configured or invalid.",
|
this.correlationId
|
);
|
height = BrowserConstants.POPUP_HEIGHT;
|
}
|
|
if (!top || top < 0 || top > winHeight) {
|
this.logger.verbose(
|
"Default popup window top position used. Window top not configured or invalid.",
|
this.correlationId
|
);
|
top = Math.max(
|
0,
|
winHeight / 2 - BrowserConstants.POPUP_HEIGHT / 2 + winTop
|
);
|
}
|
|
if (!left || left < 0 || left > winWidth) {
|
this.logger.verbose(
|
"Default popup window left position used. Window left not configured or invalid.",
|
this.correlationId
|
);
|
left = Math.max(
|
0,
|
winWidth / 2 - BrowserConstants.POPUP_WIDTH / 2 + winLeft
|
);
|
}
|
|
return popupWindowParent.open(
|
urlNavigate,
|
popupName,
|
`width=${width}, height=${height}, top=${top}, left=${left}, scrollbars=yes`
|
);
|
}
|
|
/**
|
* Generates the name for the popup based on the client id and request
|
* @param clientId
|
* @param request
|
*/
|
generatePopupName(scopes: Array<string>, authority: string): string {
|
return `${BrowserConstants.POPUP_NAME_PREFIX}.${
|
this.config.auth.clientId
|
}.${scopes.join("-")}.${authority}.${this.correlationId}`;
|
}
|
|
/**
|
* Generates the name for the popup based on the client id and request for logouts
|
* @param clientId
|
* @param request
|
*/
|
generateLogoutPopupName(request: CommonEndSessionRequest): string {
|
const homeAccountId = request.account && request.account.homeAccountId;
|
return `${BrowserConstants.POPUP_NAME_PREFIX}.${this.config.auth.clientId}.${homeAccountId}.${this.correlationId}`;
|
}
|
}
|