UNPKG

msal-iframe-ok

Version:

Fork to allow silent renewal in iFrame of Microsoft Authentication Library for js

955 lines (954 loc) 106 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, SSOTypes, PromptState } from "./Constants"; import { IdToken } from "./IdToken"; import { Storage } from "./Storage"; import { Account } from "./Account"; import { Utils } from "./Utils"; import { AuthorityFactory } from "./AuthorityFactory"; import { buildConfiguration } from "./Configuration"; import { validateClaimsRequest } from "./AuthenticationParameters"; 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"; // 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" }; /** * 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; // 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 isCallback = this.isCallback(urlHash); // On the server 302 - Redirect, handle this if (!this.config.framework.isAngular) { if (isCallback) { 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.redirectSuccessHandler = function (response) { if (this.errorReceivedCallback) { this.tokenReceivedCallback(response); } else if (this.authResponseCallback) { this.authResponseCallback(null, response); } }; UserAgentApplication.prototype.redirectErrorHandler = function (authErr, response) { if (this.errorReceivedCallback) { this.errorReceivedCallback(authErr, response.accountState); } else { this.authResponseCallback(authErr, response); } }; //#endregion //#region Redirect Flow /** * 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) { var _this = this; // Throw error if callbacks are not set before redirect if (!this.redirectCallbacksSet) { throw ClientConfigurationError.createRedirectCallbacksNotSetError(); } // Creates navigate url; saves value in cache; redirect user to AAD if (this.loginInProgress) { this.redirectErrorHandler(ClientAuthError.createLoginInProgressError(), buildResponseStateOnly(request && request.state)); return; } // if extraScopesToConsent is passed, append them to the login request var scopes = this.appendScopes(request); // Validate and filter scopes (the validate function will throw if validation fails) this.validateInputScope(scopes, false); var account = this.getAccount(); // defer queryParameters generation to Helper if developer passes account/sid/login_hint if (Utils.isSSOParam(request)) { // if account is not provided, we pass null this.loginRedirectHelper(account, request, scopes); } // else handle the library data else { // 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"); if (_this.redirectCallbacksSet) { _this.redirectSuccessHandler(response); } return; }, function (error) { _this.silentLogin = false; _this.logger.error("Error occurred during unified cache ATS"); // call the loginRedirectHelper later with no user account context _this.loginRedirectHelper(null, request, scopes); }); } // else proceed to login else { // call the loginRedirectHelper later with no user account context this.loginRedirectHelper(null, request, scopes); } } }; /** * @hidden * @ignore * Helper function to loginRedirect * * @param account * @param AuthenticationParameters * @param scopes */ UserAgentApplication.prototype.loginRedirectHelper = function (account, request, scopes) { var _this = this; // Track login in progress this.loginInProgress = true; this.authorityInstance.resolveEndpointsAsync().then(function () { // create the Request to be sent to the Server var serverAuthenticationRequest = new ServerRequestParameters(_this.authorityInstance, _this.clientId, scopes, ResponseTypes.id_token, _this.getRedirectUri(), request && request.state); // populate QueryParameters (sid/login_hint/domain_hint) and any other extraQueryParameters set by the developer serverAuthenticationRequest = _this.populateQueryParams(account, request, serverAuthenticationRequest); // if the user sets the login start page - angular only?? var loginStartPage = _this.cacheStorage.getItem(Constants.angularLoginRequest); if (!loginStartPage || loginStartPage === "") { loginStartPage = window.location.href; } else { _this.cacheStorage.setItem(Constants.angularLoginRequest, ""); } _this.updateCacheEntries(serverAuthenticationRequest, account, loginStartPage); // build URL to navigate to proceed with the login var urlNavigate = serverAuthenticationRequest.createNavigateUrl(scopes) + Constants.response_mode_fragment; // Redirect user to login URL _this.promptUser(urlNavigate); }).catch(function (err) { _this.logger.warning("could not resolve endpoints"); _this.redirectErrorHandler(ClientAuthError.createEndpointResolutionError(err.toString), buildResponseStateOnly(request && request.state)); }); }; /** * 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) { var _this = this; // Throw error if callbacks are not set before redirect if (!this.redirectCallbacksSet) { throw ClientConfigurationError.createRedirectCallbacksNotSetError(); } // Validate and filter scopes (the validate function will throw if validation fails) this.validateInputScope(request.scopes, true); // Get the account object if a session exists var account = request.account || this.getAccount(); // If already in progress, do not proceed if (this.acquireTokenInProgress) { this.redirectErrorHandler(ClientAuthError.createAcquireTokenInProgressError(), buildResponseStateOnly(this.getAccountState(request.state))); return; } // If no session exists, prompt the user to login. if (!account && !(request.sid || request.loginHint)) { this.logger.info("User login is required"); throw ClientAuthError.createUserLoginRequiredError(); } var serverAuthenticationRequest; var acquireTokenAuthority = request.authority ? AuthorityFactory.CreateInstance(request.authority, this.config.auth.validateAuthority) : this.authorityInstance; // Track the acquireToken progress this.acquireTokenInProgress = true; acquireTokenAuthority.resolveEndpointsAsync().then(function () { // On Fulfillment var responseType = _this.getTokenType(account, request.scopes, false); serverAuthenticationRequest = new ServerRequestParameters(acquireTokenAuthority, _this.clientId, request.scopes, responseType, _this.getRedirectUri(), request.state); _this.updateCacheEntries(serverAuthenticationRequest, account); // populate QueryParameters (sid/login_hint/domain_hint) and any other extraQueryParameters set by the developer serverAuthenticationRequest = _this.populateQueryParams(account, request, serverAuthenticationRequest); // Construct urlNavigate var urlNavigate = serverAuthenticationRequest.createNavigateUrl(request.scopes) + Constants.response_mode_fragment; // set state in cache and redirect to urlNavigate if (urlNavigate) { _this.cacheStorage.setItem(Constants.stateAcquireToken, serverAuthenticationRequest.state, _this.inCookie); window.location.replace(urlNavigate); } }).catch(function (err) { _this.logger.warning("could not resolve endpoints"); _this.redirectErrorHandler(ClientAuthError.createEndpointResolutionError(err.toString), buildResponseStateOnly(request.state)); }); }; /** * @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. */ // TODO - rename this, the name is confusing UserAgentApplication.prototype.isCallback = function (hash) { hash = this.getHash(hash); var parameters = Utils.deserialize(hash); return (parameters.hasOwnProperty(Constants.errorDescription) || parameters.hasOwnProperty(Constants.error) || parameters.hasOwnProperty(Constants.accessToken) || parameters.hasOwnProperty(Constants.idToken)); }; //#endregion //#region Popup Flow /** * 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; // Creates navigate url; saves value in cache; redirect user to AAD return new Promise(function (resolve, reject) { // Fail if login is already in progress if (_this.loginInProgress) { return reject(ClientAuthError.createLoginInProgressError()); } // if extraScopesToConsent is passed, append them to the login request var scopes = _this.appendScopes(request); // Validate and filter scopes (the validate function will throw if validation fails) _this.validateInputScope(scopes, false); var account = _this.getAccount(); // add the prompt parameter to the 'extraQueryParameters' if passed if (Utils.isSSOParam(request)) { // if account is not provided, we pass null _this.loginPopupHelper(account, resolve, reject, request, scopes); } // else handle the library data else { // Extract ADAL id_token if it 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"); resolve(response); }, function (error) { _this.silentLogin = false; _this.logger.error("Error occurred during unified cache ATS"); _this.loginPopupHelper(null, resolve, reject, request, scopes); }); } // else proceed with login else { _this.loginPopupHelper(null, resolve, reject, request, scopes); } } }); }; /** * @hidden * Helper function to loginPopup * * @param account * @param request * @param resolve * @param reject * @param scopes */ UserAgentApplication.prototype.loginPopupHelper = function (account, resolve, reject, request, scopes) { var _this = this; if (!scopes) { scopes = [this.clientId]; } var scope = scopes.join(" ").toLowerCase(); // Generate a popup window var popUpWindow = this.openWindow("about:blank", "_blank", 1, this, resolve, reject); if (!popUpWindow) { // We pass reject in openWindow, we reject there during an error return; } // Track login progress this.loginInProgress = true; // Resolve endpoint this.authorityInstance.resolveEndpointsAsync().then(function () { var serverAuthenticationRequest = new ServerRequestParameters(_this.authorityInstance, _this.clientId, scopes, ResponseTypes.id_token, _this.getRedirectUri(), request && request.state); // populate QueryParameters (sid/login_hint/domain_hint) and any other extraQueryParameters set by the developer; serverAuthenticationRequest = _this.populateQueryParams(account, request, serverAuthenticationRequest); _this.updateCacheEntries(serverAuthenticationRequest, account, window.location.href); // Cache the state, nonce, and login request data _this.cacheStorage.setItem(Constants.loginRequest, window.location.href, _this.inCookie); _this.cacheStorage.setItem(Constants.loginError, ""); _this.cacheStorage.setItem(Constants.nonceIdToken, serverAuthenticationRequest.nonce, _this.inCookie); _this.cacheStorage.setItem(Constants.msalError, ""); _this.cacheStorage.setItem(Constants.msalErrorDescription, ""); // cache authorityKey _this.setAuthorityCache(serverAuthenticationRequest.state, _this.authority); // Build the URL to navigate to in the popup window var urlNavigate = serverAuthenticationRequest.createNavigateUrl(scopes) + Constants.response_mode_fragment; window.renewStates.push(serverAuthenticationRequest.state); window.requestType = Constants.login; // Register callback to capture results from server _this.registerCallback(serverAuthenticationRequest.state, scope, resolve, reject); // Navigate url in popupWindow if (popUpWindow) { _this.logger.infoPii("Navigated Popup window to:" + urlNavigate); popUpWindow.location.href = urlNavigate; } }, function () { // Endpoint resolution failure error _this.logger.info(ClientAuthErrorMessage.endpointResolutionError.code + ":" + ClientAuthErrorMessage.endpointResolutionError.desc); _this.cacheStorage.setItem(Constants.msalError, ClientAuthErrorMessage.endpointResolutionError.code); _this.cacheStorage.setItem(Constants.msalErrorDescription, ClientAuthErrorMessage.endpointResolutionError.desc); // reject that is passed in - REDO this in the subsequent refactor, passing reject is confusing if (reject) { reject(ClientAuthError.createEndpointResolutionError()); } // Close the popup window if (popUpWindow) { popUpWindow.close(); } // this is an all catch for any failure for the above code except the specific 'reject' call }).catch(function (err) { _this.logger.warning("could not resolve endpoints"); reject(ClientAuthError.createEndpointResolutionError(err.toString)); }); }; /** * 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; 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(); // Get the account object if a session exists var account = request.account || _this.getAccount(); // If already in progress, throw an error and reject the request if (_this.acquireTokenInProgress) { return reject(ClientAuthError.createAcquireTokenInProgressError()); } // If no session exists, prompt the user to login. if (!account && !(request.sid || request.loginHint)) { _this.logger.info("User login is required"); return reject(ClientAuthError.createUserLoginRequiredError()); } // track the acquireToken progress _this.acquireTokenInProgress = true; var serverAuthenticationRequest; var acquireTokenAuthority = request.authority ? AuthorityFactory.CreateInstance(request.authority, _this.config.auth.validateAuthority) : _this.authorityInstance; // Open the popup window var popUpWindow = _this.openWindow("about:blank", "_blank", 1, _this, resolve, reject); if (!popUpWindow) { // We pass reject to openWindow, so we are rejecting there. return; } acquireTokenAuthority.resolveEndpointsAsync().then(function () { // On fullfillment var responseType = _this.getTokenType(account, request.scopes, false); serverAuthenticationRequest = new ServerRequestParameters(acquireTokenAuthority, _this.clientId, request.scopes, responseType, _this.getRedirectUri(), request.state); // populate QueryParameters (sid/login_hint/domain_hint) and any other extraQueryParameters set by the developer serverAuthenticationRequest = _this.populateQueryParams(account, request, serverAuthenticationRequest); _this.updateCacheEntries(serverAuthenticationRequest, account); // Construct the urlNavigate var urlNavigate = serverAuthenticationRequest.createNavigateUrl(request.scopes) + Constants.response_mode_fragment; window.renewStates.push(serverAuthenticationRequest.state); window.requestType = Constants.renewToken; _this.registerCallback(serverAuthenticationRequest.state, scope, resolve, reject); // open popup window to urlNavigate if (popUpWindow) { popUpWindow.location.href = urlNavigate; } }, function () { // Endpoint resolution failure error _this.logger.info(ClientAuthErrorMessage.endpointResolutionError.code + ":" + ClientAuthErrorMessage.endpointResolutionError.desc); _this.cacheStorage.setItem(Constants.msalError, ClientAuthErrorMessage.endpointResolutionError.code); _this.cacheStorage.setItem(Constants.msalErrorDescription, ClientAuthErrorMessage.endpointResolutionError.desc); // reject that is passed in - REDO this in the subsequent refactor, passing reject is confusing if (reject) { reject(ClientAuthError.createEndpointResolutionError()); } if (popUpWindow) { popUpWindow.close(); } // this is an all catch for any failure for the above code except the specific 'reject' call }).catch(function (err) { _this.logger.warning("could not resolve endpoints"); reject(ClientAuthError.createEndpointResolutionError(err.toString())); }); }); }; /** * @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 Silent Flow /** * 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; 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 him 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) && Utils.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 (Utils.isSSOParam(request) || account) { serverAuthenticationRequest = _this.populateQueryParams(account, request, serverAuthenticationRequest); } //if user didn't pass login_hint/sid and adal's idtoken is present, extract the login_hint from the adalIdToken else if (!account && !Utils.isEmpty(adalIdToken)) { // if adalIdToken exists, extract the SSO info from the same var adalIdTokenObject = Utils.extractIdToken(adalIdToken); _this.logger.verbose("ADAL's idToken exists. Extracting login information from ADAL's idToken "); serverAuthenticationRequest = _this.populateQueryParams(account, null, serverAuthenticationRequest, adalIdTokenObject); } var userContainedClaims = request.claimsRequest || serverAuthenticationRequest.claimsValue; var authErr; var cacheResultResponse; if (!userContainedClaims) { 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 { if (userContainedClaims) { _this.logger.verbose("Skipped cache lookup since claims were given."); } else { _this.logger.verbose("Token is not in cache for scope:" + scope); } // 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.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; }); } }); }; /** * @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 */ UserAgentApplication.prototype.isInteractionRequired = function (errorString) { if (errorString.indexOf("interaction_required") !== -1 || errorString.indexOf("consent_required") !== -1 || errorString.indexOf("login_required") !== -1) { return true; } return false; }; /** * @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 * * Adds login_hint to authorization URL which is used to pre-fill the username field of sign in page for the user if known ahead of time * domain_hint can be one of users/organizations which when added skips the email based discovery process of the user * domain_req utid received as part of the clientInfo * login_req uid received as part of clientInfo * Also does a sanity check for extraQueryParameters passed by the user to ensure no repeat queryParameters * * @param {@link Account} account - Account for which the token is requested * @param queryparams * @param {@link ServerRequestParameters} * @ignore */ UserAgentApplication.prototype.addHintParameters = function (accountObj, qParams, serverReqParams) { var account = accountObj || this.getAccount(); // This is a final check for all queryParams added so far; preference order: sid > login_hint // sid cannot be passed along with login_hint or domain_hint, hence we check both are not populated yet in queryParameters if (account && !qParams[SSOTypes.SID]) { // sid - populate only if login_hint is not already populated and the account has sid var populateSID = !qParams[SSOTypes.LOGIN_HINT] && account.sid && serverReqParams.promptValue === PromptState.NONE; if (populateSID) { qParams = Utils.addSSOParameter(SSOTypes.SID, account.sid, qParams); } // login_hint - account.userName else { var populateLoginHint = !qParams[SSOTypes.LOGIN_HINT] && account.userName && !Utils.isEmpty(account.userName); if (populateLoginHint) { qParams = Utils.addSSOParameter(SSOTypes.LOGIN_HINT, account.userName, qParams); } } var populateReqParams = !qParams[SSOTypes.DOMAIN_REQ] && !qParams[SSOTypes.LOGIN_REQ]; if (populateReqParams) { qParams = Utils.addSSOParameter(SSOTypes.HOMEACCOUNT_ID, account.homeAccountIdentifier, qParams); } } return qParams; }; /** * @hidden * Used to redirect the browser to the STS authorization endpoint * @param {string} urlNavigate - URL of the authorization endpoint */ UserAgentApplication.prototype.promptUser = function (urlNavigate) { // Navigate if valid URL if (urlNavigate && !Utils.isEmpty(urlNavigate)) { this.logger.infoPii("Navigate to:" + urlNavigate); window.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 `wi