msal-iframe-ok
Version:
Fork to allow silent renewal in iFrame of Microsoft Authentication Library for js
955 lines (954 loc) • 106 kB
JavaScript
// 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/<Enter_the_Tenant_Info_Here>.
* 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://<instance>/tfp/<tenant>/<policyName>/
* @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