UNPKG

msal

Version:
1,005 lines (1,004 loc) 97 kB
// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as tslib_1 from "tslib"; import { AccessTokenKey } from "./AccessTokenKey"; import { AccessTokenValue } from "./AccessTokenValue"; import { ServerRequestParameters } from "./ServerRequestParameters"; import { ClientInfo } from "./ClientInfo"; import { Constants, libraryVersion } from "./utils/Constants"; import { IdToken } from "./IdToken"; import { Storage } from "./Storage"; import { Account } from "./Account"; import { ScopeSet } from "./ScopeSet"; import { StringUtils } from "./utils/StringUtils"; import { CryptoUtils } from "./utils/CryptoUtils"; import { TokenUtils } from "./utils/TokenUtils"; import { TimeUtils } from "./utils/TimeUtils"; import { UrlUtils } from "./utils/UrlUtils"; import { ResponseUtils } from "./utils/ResponseUtils"; import { AuthorityFactory } from "./AuthorityFactory"; import { buildConfiguration } from "./Configuration"; import { ClientConfigurationError } from "./error/ClientConfigurationError"; import { AuthError } from "./error/AuthError"; import { ClientAuthError, ClientAuthErrorMessage } from "./error/ClientAuthError"; import { ServerError } from "./error/ServerError"; import { InteractionRequiredAuthError } from "./error/InteractionRequiredAuthError"; import { buildResponseStateOnly } from "./AuthResponse"; import TelemetryManager from "./telemetry/TelemetryManager"; // default authority var DEFAULT_AUTHORITY = "https://login.microsoftonline.com/common"; /** * @hidden * @ignore * response_type from OpenIDConnect * References: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html & https://tools.ietf.org/html/rfc6749#section-4.2.1 * Since we support only implicit flow in this library, we restrict the response_type support to only 'token' and 'id_token' * */ var ResponseTypes = { id_token: "id_token", token: "token", id_token_token: "id_token token" }; /** * @hidden * @ignore * A wrapper to handle the token response/error within the iFrame always * * @param target * @param propertyKey * @param descriptor */ var resolveTokenOnlyIfOutOfIframe = function (target, propertyKey, descriptor) { var tokenAcquisitionMethod = descriptor.value; descriptor.value = function () { var args = []; for (var _i = 0; _i < arguments.length; _i++) { args[_i] = arguments[_i]; } return this.isInIframe() ? new Promise(function () { return; }) : tokenAcquisitionMethod.apply(this, args); }; return descriptor; }; /** * UserAgentApplication class * * Object Instance that the developer can use to make loginXX OR acquireTokenXX functions */ var UserAgentApplication = /** @class */ (function () { /** * @constructor * Constructor for the UserAgentApplication used to instantiate the UserAgentApplication object * * Important attributes in the Configuration object for auth are: * - clientID: the application ID of your application. * You can obtain one by registering your application with our Application registration portal : https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredAppsPreview * - authority: the authority URL for your application. * * In Azure AD, authority is a URL indicating the Azure active directory that MSAL uses to obtain tokens. * It is of the form https://login.microsoftonline.com/&lt;Enter_the_Tenant_Info_Here&gt;. * If your application supports Accounts in one organizational directory, replace "Enter_the_Tenant_Info_Here" value with the Tenant Id or Tenant name (for example, contoso.microsoft.com). * If your application supports Accounts in any organizational directory, replace "Enter_the_Tenant_Info_Here" value with organizations. * If your application supports Accounts in any organizational directory and personal Microsoft accounts, replace "Enter_the_Tenant_Info_Here" value with common. * To restrict support to Personal Microsoft accounts only, replace "Enter_the_Tenant_Info_Here" value with consumers. * * * In Azure B2C, authority is of the form https://&lt;instance&gt;/tfp/&lt;tenant&gt;/&lt;policyName&gt;/ * @param {@link (Configuration:type)} configuration object for the MSAL UserAgentApplication instance */ function UserAgentApplication(configuration) { // callbacks for token/error this.authResponseCallback = null; this.tokenReceivedCallback = null; this.errorReceivedCallback = null; // Set the Configuration this.config = buildConfiguration(configuration); // Set the callback boolean this.redirectCallbacksSet = false; this.logger = this.config.system.logger; this.clientId = this.config.auth.clientId; this.inCookie = this.config.cache.storeAuthStateInCookie; this.telemetryManager = this.getTelemetryManagerFromConfig(this.config.system.telemetry, this.clientId); // if no authority is passed, set the default: "https://login.microsoftonline.com/common" this.authority = this.config.auth.authority || DEFAULT_AUTHORITY; // track login and acquireToken in progress this.loginInProgress = false; this.acquireTokenInProgress = false; // cache keys msal - typescript throws an error if any value other than "localStorage" or "sessionStorage" is passed try { this.cacheStorage = new Storage(this.config.cache.cacheLocation); } catch (e) { throw ClientConfigurationError.createInvalidCacheLocationConfigError(this.config.cache.cacheLocation); } // Initialize window handling code window.openedWindows = []; window.activeRenewals = {}; window.renewStates = []; window.callbackMappedToRenewStates = {}; window.promiseMappedToRenewStates = {}; window.msal = this; var urlHash = window.location.hash; var urlContainsHash = this.urlContainsHash(urlHash); // On the server 302 - Redirect, handle this if (!this.config.framework.isAngular) { if (urlContainsHash) { this.handleAuthenticationResponse(urlHash); } } } Object.defineProperty(UserAgentApplication.prototype, "authority", { /** * Method to manage the authority URL. * * @returns {string} authority */ get: function () { return this.authorityInstance.CanonicalAuthority; }, /** * setter for the authority URL * @param {string} authority */ // If the developer passes an authority, create an instance set: function (val) { this.authorityInstance = AuthorityFactory.CreateInstance(val, this.config.auth.validateAuthority); }, enumerable: true, configurable: true }); /** * Get the current authority instance from the MSAL configuration object * * @returns {@link Authority} authority instance */ UserAgentApplication.prototype.getAuthorityInstance = function () { return this.authorityInstance; }; UserAgentApplication.prototype.handleRedirectCallback = function (authOrTokenCallback, errorReceivedCallback) { if (!authOrTokenCallback) { this.redirectCallbacksSet = false; throw ClientConfigurationError.createInvalidCallbackObjectError(authOrTokenCallback); } // Set callbacks if (errorReceivedCallback) { this.tokenReceivedCallback = authOrTokenCallback; this.errorReceivedCallback = errorReceivedCallback; this.logger.warning("This overload for callback is deprecated - please change the format of the callbacks to a single callback as shown: (err: AuthError, response: AuthResponse)."); } else { this.authResponseCallback = authOrTokenCallback; } this.redirectCallbacksSet = true; // On the server 302 - Redirect, handle this if (!this.config.framework.isAngular) { var cachedHash = this.cacheStorage.getItem(Constants.urlHash); if (cachedHash) { this.processCallBack(cachedHash, null); } } }; UserAgentApplication.prototype.authResponseHandler = function (interactionType, response, resolve) { if (interactionType === Constants.interactionTypeRedirect) { if (this.errorReceivedCallback) { this.tokenReceivedCallback(response); } else if (this.authResponseCallback) { this.authResponseCallback(null, response); } } else if (interactionType === Constants.interactionTypePopup) { resolve(response); } else { throw ClientAuthError.createInvalidInteractionTypeError(); } }; UserAgentApplication.prototype.authErrorHandler = function (interactionType, authErr, response, reject) { if (interactionType === Constants.interactionTypeRedirect) { if (this.errorReceivedCallback) { this.errorReceivedCallback(authErr, response.accountState); } else { this.authResponseCallback(authErr, response); } } else if (interactionType === Constants.interactionTypePopup) { reject(authErr); } else { throw ClientAuthError.createInvalidInteractionTypeError(); } }; //#endregion /** * Use when initiating the login process by redirecting the user's browser to the authorization endpoint. * @param {@link (AuthenticationParameters:type)} */ UserAgentApplication.prototype.loginRedirect = function (request) { // Throw error if callbacks are not set before redirect if (!this.redirectCallbacksSet) { throw ClientConfigurationError.createRedirectCallbacksNotSetError(); } this.acquireTokenInteractive(Constants.interactionTypeRedirect, true, request); }; /** * Use when you want to obtain an access_token for your API by redirecting the user's browser window to the authorization endpoint. * @param {@link (AuthenticationParameters:type)} * * To renew idToken, please pass clientId as the only scope in the Authentication Parameters */ UserAgentApplication.prototype.acquireTokenRedirect = function (request) { if (!request) { throw ClientConfigurationError.createEmptyRequestError(); } // Throw error if callbacks are not set before redirect if (!this.redirectCallbacksSet) { throw ClientConfigurationError.createRedirectCallbacksNotSetError(); } this.acquireTokenInteractive(Constants.interactionTypeRedirect, false, request); }; /** * Use when initiating the login process via opening a popup window in the user's browser * * @param {@link (AuthenticationParameters:type)} * * @returns {Promise.<AuthResponse>} - a promise that is fulfilled when this function has completed, or rejected if an error was raised. Returns the {@link AuthResponse} object */ UserAgentApplication.prototype.loginPopup = function (request) { var _this = this; return new Promise(function (resolve, reject) { _this.acquireTokenInteractive(Constants.interactionTypePopup, true, request, resolve, reject); }); }; /** * Use when you want to obtain an access_token for your API via opening a popup window in the user's browser * @param {@link AuthenticationParameters} * * To renew idToken, please pass clientId as the only scope in the Authentication Parameters * @returns {Promise.<AuthResponse>} - a promise that is fulfilled when this function has completed, or rejected if an error was raised. Returns the {@link AuthResponse} object */ UserAgentApplication.prototype.acquireTokenPopup = function (request) { var _this = this; if (!request) { throw ClientConfigurationError.createEmptyRequestError(); } return new Promise(function (resolve, reject) { _this.acquireTokenInteractive(Constants.interactionTypePopup, false, request, resolve, reject); }); }; //#region Acquire Token /** * Use when initiating the login process or when you want to obtain an access_token for your API, * either by redirecting the user's browser window to the authorization endpoint or via opening a popup window in the user's browser. * @param {@link (AuthenticationParameters:type)} * * To renew idToken, please pass clientId as the only scope in the Authentication Parameters */ UserAgentApplication.prototype.acquireTokenInteractive = function (interactionType, isLoginCall, request, resolve, reject) { var _this = this; // If already in progress, do not proceed if (this.loginInProgress || this.acquireTokenInProgress) { var thrownError = this.loginInProgress ? ClientAuthError.createLoginInProgressError() : ClientAuthError.createAcquireTokenInProgressError(); var stateOnlyResponse = buildResponseStateOnly(this.getAccountState(request && request.state)); this.authErrorHandler(interactionType, thrownError, stateOnlyResponse, reject); return; } // if extraScopesToConsent is passed in loginCall, append them to the login request var scopes = isLoginCall ? this.appendScopes(request) : request.scopes; // Validate and filter scopes (the validate function will throw if validation fails) this.validateInputScope(scopes, !isLoginCall); // Get the account object if a session exists var account = (request && request.account && !isLoginCall) ? request.account : this.getAccount(); // If no session exists, prompt the user to login. if (!account && !ServerRequestParameters.isSSOParam(request)) { if (isLoginCall) { // extract ADAL id_token if exists var adalIdToken = this.extractADALIdToken(); // silent login if ADAL id_token is retrieved successfully - SSO if (adalIdToken && !scopes) { this.logger.info("ADAL's idToken exists. Extracting login information from ADAL's idToken "); var tokenRequest = this.buildIDTokenRequest(request); this.silentLogin = true; this.acquireTokenSilent(tokenRequest).then(function (response) { _this.silentLogin = false; _this.logger.info("Unified cache call is successful"); _this.authResponseHandler(interactionType, response, resolve); return; }, function (error) { _this.silentLogin = false; _this.logger.error("Error occurred during unified cache ATS: " + error); // proceed to login since ATS failed _this.acquireTokenHelper(null, interactionType, isLoginCall, request, scopes, resolve, reject); }); } // No ADAL token found, proceed to login else { this.acquireTokenHelper(null, interactionType, isLoginCall, request, scopes, resolve, reject); } } // AcquireToken call, but no account or context given, so throw error else { this.logger.info("User login is required"); throw ClientAuthError.createUserLoginRequiredError(); } } // User session exists else { this.acquireTokenHelper(account, interactionType, isLoginCall, request, scopes, resolve, reject); } }; /** * @hidden * @ignore * Helper function to acquireToken * */ UserAgentApplication.prototype.acquireTokenHelper = function (account, interactionType, isLoginCall, request, scopes, resolve, reject) { var _this = this; // Track the acquireToken progress if (isLoginCall) { this.loginInProgress = true; } else { this.acquireTokenInProgress = true; } var scope = scopes ? scopes.join(" ").toLowerCase() : this.clientId.toLowerCase(); var serverAuthenticationRequest; var acquireTokenAuthority = (!isLoginCall && request && request.authority) ? AuthorityFactory.CreateInstance(request.authority, this.config.auth.validateAuthority) : this.authorityInstance; var popUpWindow; if (interactionType === Constants.interactionTypePopup) { // Generate a popup window popUpWindow = this.openWindow("about:blank", "_blank", 1, this, resolve, reject); if (!popUpWindow) { // We pass reject in openWindow, we reject there during an error return; } } acquireTokenAuthority.resolveEndpointsAsync().then(function () { // On Fulfillment var responseType = isLoginCall ? ResponseTypes.id_token : _this.getTokenType(account, scopes, false); var loginStartPage; if (isLoginCall) { // if the user sets the login start page - angular only?? loginStartPage = _this.cacheStorage.getItem(Constants.angularLoginRequest); if (!loginStartPage || loginStartPage === "") { loginStartPage = window.location.href; } else { _this.cacheStorage.setItem(Constants.angularLoginRequest, ""); } } serverAuthenticationRequest = new ServerRequestParameters(acquireTokenAuthority, _this.clientId, scopes, responseType, _this.getRedirectUri(), request && request.state); _this.updateCacheEntries(serverAuthenticationRequest, account, loginStartPage); // populate QueryParameters (sid/login_hint/domain_hint) and any other extraQueryParameters set by the developer serverAuthenticationRequest.populateQueryParams(account, request); // Construct urlNavigate var urlNavigate = UrlUtils.createNavigateUrl(serverAuthenticationRequest) + Constants.response_mode_fragment; // set state in cache if (interactionType === Constants.interactionTypeRedirect) { if (!isLoginCall) { _this.cacheStorage.setItem(Constants.stateAcquireToken, serverAuthenticationRequest.state, _this.inCookie); } } else if (interactionType === Constants.interactionTypePopup) { window.renewStates.push(serverAuthenticationRequest.state); window.requestType = isLoginCall ? Constants.login : Constants.renewToken; // Register callback to capture results from server _this.registerCallback(serverAuthenticationRequest.state, scope, resolve, reject); } else { throw ClientAuthError.createInvalidInteractionTypeError(); } // prompt user for interaction _this.navigateWindow(urlNavigate, popUpWindow); }).catch(function (err) { _this.logger.warning("could not resolve endpoints"); _this.authErrorHandler(interactionType, ClientAuthError.createEndpointResolutionError(err.toString), buildResponseStateOnly(request.state), reject); if (popUpWindow) { popUpWindow.close(); } }); }; /** * Use this function to obtain a token before every call to the API / resource provider * * MSAL return's a cached token when available * Or it send's a request to the STS to obtain a new token using a hidden iframe. * * @param {@link AuthenticationParameters} * * To renew idToken, please pass clientId as the only scope in the Authentication Parameters * @returns {Promise.<AuthResponse>} - a promise that is fulfilled when this function has completed, or rejected if an error was raised. Returns the {@link AuthResponse} object * */ UserAgentApplication.prototype.acquireTokenSilent = function (request) { var _this = this; if (!request) { throw ClientConfigurationError.createEmptyRequestError(); } return new Promise(function (resolve, reject) { // Validate and filter scopes (the validate function will throw if validation fails) _this.validateInputScope(request.scopes, true); var scope = request.scopes.join(" ").toLowerCase(); // if the developer passes an account, give that account the priority var account = request.account || _this.getAccount(); // extract if there is an adalIdToken stashed in the cache var adalIdToken = _this.cacheStorage.getItem(Constants.adalIdToken); //if there is no account logged in and no login_hint/sid is passed in the request if (!account && !(request.sid || request.loginHint) && StringUtils.isEmpty(adalIdToken)) { _this.logger.info("User login is required"); return reject(ClientAuthError.createUserLoginRequiredError()); } var responseType = _this.getTokenType(account, request.scopes, true); var serverAuthenticationRequest = new ServerRequestParameters(AuthorityFactory.CreateInstance(request.authority, _this.config.auth.validateAuthority), _this.clientId, request.scopes, responseType, _this.getRedirectUri(), request && request.state); // populate QueryParameters (sid/login_hint/domain_hint) and any other extraQueryParameters set by the developer if (ServerRequestParameters.isSSOParam(request) || account) { serverAuthenticationRequest.populateQueryParams(account, request); } //if user didn't pass login_hint/sid and adal's idtoken is present, extract the login_hint from the adalIdToken else if (!account && !StringUtils.isEmpty(adalIdToken)) { // if adalIdToken exists, extract the SSO info from the same var adalIdTokenObject = TokenUtils.extractIdToken(adalIdToken); _this.logger.verbose("ADAL's idToken exists. Extracting login information from ADAL's idToken "); serverAuthenticationRequest.populateQueryParams(account, null, adalIdTokenObject); } var userContainedClaims = request.claimsRequest || serverAuthenticationRequest.claimsValue; var authErr; var cacheResultResponse; if (!userContainedClaims && !request.forceRefresh) { try { cacheResultResponse = _this.getCachedToken(serverAuthenticationRequest, account); } catch (e) { authErr = e; } } // resolve/reject based on cacheResult if (cacheResultResponse) { _this.logger.info("Token is already in cache for scope:" + scope); resolve(cacheResultResponse); return null; } else if (authErr) { _this.logger.infoPii(authErr.errorCode + ":" + authErr.errorMessage); reject(authErr); return null; } // else proceed with login else { var logMessage = void 0; if (userContainedClaims) { logMessage = "Skipped cache lookup since claims were given."; } else if (request.forceRefresh) { logMessage = "Skipped cache lookup since request.forceRefresh option was set to true"; } else { logMessage = "Token is not in cache for scope:" + scope; } _this.logger.verbose(logMessage); // Cache result can return null if cache is empty. In that case, set authority to default value if no authority is passed to the api. if (!serverAuthenticationRequest.authorityInstance) { serverAuthenticationRequest.authorityInstance = request.authority ? AuthorityFactory.CreateInstance(request.authority, _this.config.auth.validateAuthority) : _this.authorityInstance; } // cache miss return serverAuthenticationRequest.authorityInstance.resolveEndpointsAsync() .then(function () { // refresh attempt with iframe // Already renewing for this scope, callback when we get the token. if (window.activeRenewals[scope]) { _this.logger.verbose("Renew token for scope: " + scope + " is in progress. Registering callback"); // Active renewals contains the state for each renewal. _this.registerCallback(window.activeRenewals[scope], scope, resolve, reject); } else { if (request.scopes && request.scopes.indexOf(_this.clientId) > -1 && request.scopes.length === 1) { // App uses idToken to send to api endpoints // Default scope is tracked as clientId to store this token _this.logger.verbose("renewing idToken"); _this.silentLogin = true; _this.renewIdToken(request.scopes, resolve, reject, account, serverAuthenticationRequest); } else { // renew access token _this.logger.verbose("renewing accesstoken"); _this.renewToken(request.scopes, resolve, reject, account, serverAuthenticationRequest); } } }).catch(function (err) { _this.logger.warning("could not resolve endpoints"); reject(ClientAuthError.createEndpointResolutionError(err.toString())); return null; }); } }); }; //#endregion //#region Popup Window Creation /** * @hidden * * Used to send the user to the redirect_uri after authentication is complete. The user's bearer token is attached to the URI fragment as an id_token/access_token field. * This function also closes the popup window after redirection. * * @param urlNavigate * @param title * @param interval * @param instance * @param resolve * @param reject * @ignore */ UserAgentApplication.prototype.openWindow = function (urlNavigate, title, interval, instance, resolve, reject) { var _this = this; // Generate a popup window var popupWindow; try { popupWindow = this.openPopup(urlNavigate, title, Constants.popUpWidth, Constants.popUpHeight); } catch (e) { instance.loginInProgress = false; instance.acquireTokenInProgress = false; this.logger.info(ClientAuthErrorMessage.popUpWindowError.code + ":" + ClientAuthErrorMessage.popUpWindowError.desc); this.cacheStorage.setItem(Constants.msalError, ClientAuthErrorMessage.popUpWindowError.code); this.cacheStorage.setItem(Constants.msalErrorDescription, ClientAuthErrorMessage.popUpWindowError.desc); if (reject) { reject(ClientAuthError.createPopupWindowError()); } return null; } // Push popup window handle onto stack for tracking window.openedWindows.push(popupWindow); var pollTimer = window.setInterval(function () { // If popup closed or login in progress, cancel login if (popupWindow && popupWindow.closed && (instance.loginInProgress || instance.acquireTokenInProgress)) { if (reject) { reject(ClientAuthError.createUserCancelledError()); } window.clearInterval(pollTimer); if (_this.config.framework.isAngular) { _this.broadcast("msal:popUpClosed", ClientAuthErrorMessage.userCancelledError.code + Constants.resourceDelimiter + ClientAuthErrorMessage.userCancelledError.desc); return; } instance.loginInProgress = false; instance.acquireTokenInProgress = false; } try { var popUpWindowLocation = popupWindow.location; // If the popup hash changes, close the popup window if (popUpWindowLocation.href.indexOf(_this.getRedirectUri()) !== -1) { window.clearInterval(pollTimer); instance.loginInProgress = false; instance.acquireTokenInProgress = false; _this.logger.info("Closing popup window"); // TODO: Check how this can be extracted for any framework specific code? if (_this.config.framework.isAngular) { _this.broadcast("msal:popUpHashChanged", popUpWindowLocation.hash); for (var i = 0; i < window.openedWindows.length; i++) { window.openedWindows[i].close(); } } } } catch (e) { // Cross Domain url check error. // Will be thrown until AAD redirects the user back to the app"s root page with the token. // No need to log or throw this error as it will create unnecessary traffic. } }, interval); return popupWindow; }; /** * @hidden * * Configures popup window for login. * * @param urlNavigate * @param title * @param popUpWidth * @param popUpHeight * @ignore * @hidden */ UserAgentApplication.prototype.openPopup = function (urlNavigate, title, popUpWidth, popUpHeight) { try { /** * adding winLeft and winTop to account for dual monitor * using screenLeft and screenTop for IE8 and earlier */ var winLeft = window.screenLeft ? window.screenLeft : window.screenX; var winTop = window.screenTop ? window.screenTop : window.screenY; /** * window.innerWidth displays browser window"s height and width excluding toolbars * using document.documentElement.clientWidth for IE8 and earlier */ var width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth; var height = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; var left = ((width / 2) - (popUpWidth / 2)) + winLeft; var top_1 = ((height / 2) - (popUpHeight / 2)) + winTop; // open the window var popupWindow = window.open(urlNavigate, title, "width=" + popUpWidth + ", height=" + popUpHeight + ", top=" + top_1 + ", left=" + left); if (!popupWindow) { throw ClientAuthError.createPopupWindowError(); } if (popupWindow.focus) { popupWindow.focus(); } return popupWindow; } catch (e) { this.logger.error("error opening popup " + e.message); this.loginInProgress = false; this.acquireTokenInProgress = false; throw ClientAuthError.createPopupWindowError(e.toString()); } }; //#endregion //#region Iframe Management /** * @hidden * Returns whether current window is in ifram for token renewal * @ignore */ UserAgentApplication.prototype.isInIframe = function () { return window.parent !== window; }; /** * @hidden * Returns whether parent window exists and has msal */ UserAgentApplication.prototype.parentIsMsal = function () { return window.parent !== window && window.parent.msal; }; /** * @hidden * Calling _loadFrame but with a timeout to signal failure in loadframeStatus. Callbacks are left. * registered when network errors occur and subsequent token requests for same resource are registered to the pending request. * @ignore */ UserAgentApplication.prototype.loadIframeTimeout = function (urlNavigate, frameName, scope) { var _this = this; //set iframe session to pending var expectedState = window.activeRenewals[scope]; this.logger.verbose("Set loading state to pending for: " + scope + ":" + expectedState); this.cacheStorage.setItem(Constants.renewStatus + expectedState, Constants.tokenRenewStatusInProgress); this.loadFrame(urlNavigate, frameName); setTimeout(function () { if (_this.cacheStorage.getItem(Constants.renewStatus + expectedState) === Constants.tokenRenewStatusInProgress) { // fail the iframe session if it"s in pending state _this.logger.verbose("Loading frame has timed out after: " + (_this.config.system.loadFrameTimeout / 1000) + " seconds for scope " + scope + ":" + expectedState); // Error after timeout if (expectedState && window.callbackMappedToRenewStates[expectedState]) { window.callbackMappedToRenewStates[expectedState](null, ClientAuthError.createTokenRenewalTimeoutError()); } _this.cacheStorage.setItem(Constants.renewStatus + expectedState, Constants.tokenRenewStatusCancelled); } }, this.config.system.loadFrameTimeout); }; /** * @hidden * Loads iframe with authorization endpoint URL * @ignore */ UserAgentApplication.prototype.loadFrame = function (urlNavigate, frameName) { var _this = this; // This trick overcomes iframe navigation in IE // IE does not load the page consistently in iframe this.logger.info("LoadFrame: " + frameName); var frameCheck = frameName; setTimeout(function () { var frameHandle = _this.addHiddenIFrame(frameCheck); if (frameHandle.src === "" || frameHandle.src === "about:blank") { frameHandle.src = urlNavigate; _this.logger.infoPii("Frame Name : " + frameName + " Navigated to: " + urlNavigate); } }, this.config.system.navigateFrameWait); }; /** * @hidden * Adds the hidden iframe for silent token renewal. * @ignore */ UserAgentApplication.prototype.addHiddenIFrame = function (iframeId) { if (typeof iframeId === "undefined") { return null; } this.logger.info("Add msal frame to document:" + iframeId); var adalFrame = document.getElementById(iframeId); if (!adalFrame) { if (document.createElement && document.documentElement && (window.navigator.userAgent.indexOf("MSIE 5.0") === -1)) { var ifr = document.createElement("iframe"); ifr.setAttribute("id", iframeId); ifr.style.visibility = "hidden"; ifr.style.position = "absolute"; ifr.style.width = ifr.style.height = "0"; ifr.style.border = "0"; adalFrame = document.getElementsByTagName("body")[0].appendChild(ifr); } else if (document.body && document.body.insertAdjacentHTML) { document.body.insertAdjacentHTML("beforeend", "<iframe name='" + iframeId + "' id='" + iframeId + "' style='display:none'></iframe>"); } if (window.frames && window.frames[iframeId]) { adalFrame = window.frames[iframeId]; } } return adalFrame; }; //#endregion //#region General Helpers /** * @hidden * Used to redirect the browser to the STS authorization endpoint * @param {string} urlNavigate - URL of the authorization endpoint */ UserAgentApplication.prototype.navigateWindow = function (urlNavigate, popupWindow) { // Navigate if valid URL if (urlNavigate && !StringUtils.isEmpty(urlNavigate)) { var navigateWindow = popupWindow ? popupWindow : window; var logMessage = popupWindow ? "Navigated Popup window to:" + urlNavigate : "Navigate to:" + urlNavigate; this.logger.infoPii(logMessage); navigateWindow.location.replace(urlNavigate); } else { this.logger.info("Navigate url is empty"); throw AuthError.createUnexpectedError("Navigate url is empty"); } }; /** * @hidden * Used to add the developer requested callback to the array of callbacks for the specified scopes. The updated array is stored on the window object * @param {string} expectedState - Unique state identifier (guid). * @param {string} scope - Developer requested permissions. Not all scopes are guaranteed to be included in the access token returned. * @param {Function} resolve - The resolve function of the promise object. * @param {Function} reject - The reject function of the promise object. * @ignore */ UserAgentApplication.prototype.registerCallback = function (expectedState, scope, resolve, reject) { var _this = this; // track active renewals window.activeRenewals[scope] = expectedState; // initialize callbacks mapped array if (!window.promiseMappedToRenewStates[expectedState]) { window.promiseMappedToRenewStates[expectedState] = []; } // indexing on the current state, push the callback params to callbacks mapped window.promiseMappedToRenewStates[expectedState].push({ resolve: resolve, reject: reject }); // Store the server esponse in the current window?? if (!window.callbackMappedToRenewStates[expectedState]) { window.callbackMappedToRenewStates[expectedState] = function (response, error) { // reset active renewals window.activeRenewals[scope] = null; // for all promiseMappedtoRenewStates for a given 'state' - call the reject/resolve with error/token respectively for (var i = 0; i < window.promiseMappedToRenewStates[expectedState].length; ++i) { try { if (error) { window.promiseMappedToRenewStates[expectedState][i].reject(error); } else if (response) { window.promiseMappedToRenewStates[expectedState][i].resolve(response); } else { throw AuthError.createUnexpectedError("Error and response are both null"); } } catch (e) { _this.logger.warning(e); } } // reset window.promiseMappedToRenewStates[expectedState] = null; window.callbackMappedToRenewStates[expectedState] = null; }; } }; //#endregion //#region Logout /** * Use to log out the current user, and redirect the user to the postLogoutRedirectUri. * Default behaviour is to redirect the user to `window.location.href`. */ UserAgentApplication.prototype.logout = function () { var _this = this; this.clearCache(); this.account = null; var logout = ""; if (this.getPostLogoutRedirectUri()) { logout = "post_logout_redirect_uri=" + encodeURIComponent(this.getPostLogoutRedirectUri()); } this.authorityInstance.resolveEndpointsAsync().then(function (authority) { var urlNavigate = authority.EndSessionEndpoint ? authority.EndSessionEndpoint + "?" + logout : _this.authority + "oauth2/v2.0/logout?" + logout; _this.navigateWindow(urlNavigate); }); }; /** * @hidden * Clear all access tokens in the cache. * @ignore */ UserAgentApplication.prototype.clearCache = function () { window.renewStates = []; var accessTokenItems = this.cacheStorage.getAllAccessTokens(Constants.clientId, Constants.homeAccountIdentifier); for (var i = 0; i < accessTokenItems.length; i++) { this.cacheStorage.removeItem(JSON.stringify(accessTokenItems[i].key)); } this.cacheStorage.resetCacheItems(); this.cacheStorage.clearCookie(); }; /** * @hidden * Clear a given access token from the cache. * * @param accessToken */ UserAgentApplication.prototype.clearCacheForScope = function (accessToken) { var accessTokenItems = this.cacheStorage.getAllAccessTokens(Constants.clientId, Constants.homeAccountIdentifier); for (var i = 0; i < accessTokenItems.length; i++) { var token = accessTokenItems[i]; if (token.value.accessToken === accessToken) { this.cacheStorage.removeItem(JSON.stringify(token.key)); } } }; //#endregion //#region Response /** * @hidden * @ignore * Checks if the redirect response is received from the STS. In case of redirect, the url fragment has either id_token, access_token or error. * @param {string} hash - Hash passed from redirect page. * @returns {Boolean} - true if response contains id_token, access_token or error, false otherwise. */ UserAgentApplication.prototype.isCallback = function (hash) { this.logger.info("isCallback will be deprecated in favor of urlContainsHash in MSAL.js v2.0."); return this.urlContainsHash(hash); }; UserAgentApplication.prototype.urlContainsHash = function (urlString) { var parameters = this.deserializeHash(urlString); return (parameters.hasOwnProperty(Constants.errorDescription) || parameters.hasOwnProperty(Constants.error) || parameters.hasOwnProperty(Constants.accessToken) || parameters.hasOwnProperty(Constants.idToken)); }; /** * @hidden * Used to call the constructor callback with the token/error * @param {string} [hash=window.location.hash] - Hash fragment of Url. */ UserAgentApplication.prototype.processCallBack = function (hash, stateInfo, parentCallback) { this.logger.info("Processing the callback from redirect response"); // get the state info from the hash if (!stateInfo) { stateInfo = this.getResponseState(hash); } var response; var authErr; // Save the token info from the hash try { response = this.saveTokenFromHash(hash, stateInfo); } catch (err) { authErr = err; } // remove hash from the cache this.cacheStorage.removeItem(Constants.urlHash); try { // Clear the cookie in the hash this.cacheStorage.clearCookie(); var accountState = this.getAccountState(stateInfo.state); if (response) { if ((stateInfo.requestType === Constants.renewToken) || response.accessToken) { if (window.parent !== window) { this.logger.verbose("Window is in iframe, acquiring token silently"); } else { this.logger.verbose("acquiring token interactive in progress"); } response.tokenType = Constants.accessToken; } else if (stateInfo.requestType === Constants.login) { response.tokenType = Constants.idToken; } if (!parentCallback) { this.authResponseHandler(Constants.interactionTypeRedirect, response); return; } } else if (!parentCallback) { this.authErrorHandler(Constants.interactionTypeRedirect, authErr, buildResponseStateOnly(accountState)); return; } parentCallback(response, authErr); } catch (err) { this.logger.error("Error occurred in token received callback function: " + err); throw ClientAuthError.createErrorInCallbackFunction(err.toString()); } }; /** * @hidden * This method must be called for processing the response received from the STS. It extracts the hash, processes the token or error information and saves it in the cache. It then * calls the registered callbacks in case of redirect or resolves the promises with the result. * @param {string} [hash=window.location.hash] - Hash fragment of Url. */ UserAgentApplication.prototype.handleAuthenticationResponse = function (hash) { // retrieve the hash if (hash == null) { hash = window.location.hash; } var self = null; var isPopup = false; var isWindowOpenerMsal = false; // Check if the current window opened the iFrame/popup try { isWindowOpenerMsal = window.opener && window.opener.msal && window.opener.msal !== window.msal; } catch (err) { // err = SecurityError: Blocked a frame with origin "[url]" from accessing a cross-origin frame. isWindowOpenerMsal = false; } // Set the self to the window that created the popup/iframe if (isWindowOpenerMsal) { self = window.opener.msal; isPopup = true; } else if (window.parent && window.parent.msal) { self = window.parent.msal; } // if (window.parent !== window), by using self, window.parent becomes equal to window in getResponseState method specifically var stateInfo = self.getResponseState(hash); var tokenResponseCallback = null; self.logger.info("Returned from redirect url"); // If parent window is the msal instance which opened the current window (iframe) if (this.parentIsMsal()) { tokenResponseCallback = window.parent.callbackMappedToRenewStates[stateInfo.state]; } // Current window is window opener (popup) else if (isWindowOpenerMsal) { tokenResponseCallback = window.opener.callbackMappedToRenewStates[stateInfo.state]; } // Redirect cases else { tokenResponseCallback = null; // if set to navigate to loginRequest page post login if (self.config.auth.navigateToLoginRequestUrl) { self.cacheStorage.setItem(Constants.urlHash, hash); if (window.parent === window && !isPopup) { window.location.href = self.cacheStorage.getItem(Constants.loginRequest, self.inCookie); } return; } else { window.location.hash = ""; } if (!this.redirectCallbacksSet) { // We reached this point too early - cache hash, return and process in handleRedirectCallbacks self.cacheStorage.setItem(Constants.urlHash, hash); return; } } self.processCallBack(hash, stateInfo, tokenResponseCallback); // If current window is opener, close all windows if (isWindowOpenerMsal) { for (var i = 0; i < window.opener.openedWindows.length; i++) { window.opener.openedWindows[i].close(); } } }; /** * @hidden * Returns deserialized portion of URL hash * @param hash */ UserAgentApplication.prototype.deserializeHash = function (urlFragment) { var hash = UrlUtils.getHashFromUrl(urlFragment); return CryptoUtils.deserialize(hash);