msal
Version:
Microsoft Authentication Library for js
1,234 lines (1,095 loc) • 91.2 kB
text/typescript
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import { AccessTokenCacheItem } from "./AccessTokenCacheItem";
import { AccessTokenKey } from "./AccessTokenKey";
import { AccessTokenValue } from "./AccessTokenValue";
import { ServerRequestParameters } from "./ServerRequestParameters";
import { Authority } from "./Authority";
import { ClientInfo } from "./ClientInfo";
import { Constants, SSOTypes, PromptState, BlacklistedEQParams, InteractionType, libraryVersion } from "./utils/Constants";
import { IdToken } from "./IdToken";
import { Logger } from "./Logger";
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 { Configuration, buildConfiguration, TelemetryOptions } from "./Configuration";
import { AuthenticationParameters, 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 { AuthResponse, buildResponseStateOnly } from "./AuthResponse";
import TelemetryManager from "./telemetry/TelemetryManager";
import { TelemetryPlatform, TelemetryConfig } from './telemetry/TelemetryTypes';
// default authority
const DEFAULT_AUTHORITY = "https://login.microsoftonline.com/common";
/**
* Interface to handle iFrame generation, Popup Window creation and redirect handling
*/
declare global {
interface Window {
msal: Object;
CustomEvent: CustomEvent;
Event: Event;
activeRenewals: {};
renewStates: Array<string>;
callbackMappedToRenewStates : {};
promiseMappedToRenewStates: {};
openedWindows: Array<Window>;
requestType: string;
}
}
/**
* @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'
*
*/
const ResponseTypes = {
id_token: "id_token",
token: "token",
id_token_token: "id_token token"
};
/**
* @hidden
* @ignore
*/
export interface CacheResult {
errorDesc: string;
token: string;
error: string;
}
/**
* @hidden
* @ignore
* Data type to hold information about state returned from the server
*/
export type ResponseStateInfo = {
state: string;
stateMatch: boolean;
requestType: string;
};
/**
* A type alias for an authResponseCallback function.
* {@link (authResponseCallback:type)}
* @param authErr error created for failure cases
* @param response response containing token strings in success cases, or just state value in error cases
*/
export type authResponseCallback = (authErr: AuthError, response?: AuthResponse) => void;
/**
* A type alias for a tokenReceivedCallback function.
* {@link (tokenReceivedCallback:type)}
* @returns response of type {@link (AuthResponse:type)}
* The function that will get the call back once this API is completed (either successfully or with a failure).
*/
export type tokenReceivedCallback = (response: AuthResponse) => void;
/**
* A type alias for a errorReceivedCallback function.
* {@link (errorReceivedCallback:type)}
* @returns response of type {@link (AuthError:class)}
* @returns {string} account state
*/
export type errorReceivedCallback = (authErr: AuthError, accountState: string) => void;
/**
* @hidden
* @ignore
* A wrapper to handle the token response/error within the iFrame always
*
* @param target
* @param propertyKey
* @param descriptor
*/
const resolveTokenOnlyIfOutOfIframe = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const tokenAcquisitionMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
return this.isInIframe()
? new Promise(() => {
return;
})
: tokenAcquisitionMethod.apply(this, args);
};
return descriptor;
};
/**
* UserAgentApplication class
*
* Object Instance that the developer can use to make loginXX OR acquireTokenXX functions
*/
export class UserAgentApplication {
// input Configuration by the developer/user
private config: Configuration;
// callbacks for token/error
private authResponseCallback: authResponseCallback = null;
private tokenReceivedCallback: tokenReceivedCallback = null;
private errorReceivedCallback: errorReceivedCallback = null;
// Added for readability as these params are very frequently used
private logger: Logger;
private clientId: string;
private inCookie: boolean;
private telemetryManager: TelemetryManager;
// Cache and Account info referred across token grant flow
protected cacheStorage: Storage;
private account: Account;
// state variables
private loginInProgress: boolean;
private acquireTokenInProgress: boolean;
private silentAuthenticationState: string;
private silentLogin: boolean;
private redirectCallbacksSet: boolean;
// Authority Functionality
protected authorityInstance: Authority;
/**
* setter for the authority URL
* @param {string} authority
*/
// If the developer passes an authority, create an instance
public set authority(val) {
this.authorityInstance = AuthorityFactory.CreateInstance(val, this.config.auth.validateAuthority);
}
/**
* Method to manage the authority URL.
*
* @returns {string} authority
*/
public get authority(): string {
return this.authorityInstance.CanonicalAuthority;
}
/**
* Get the current authority instance from the MSAL configuration object
*
* @returns {@link Authority} authority instance
*/
public getAuthorityInstance(): Authority {
return this.authorityInstance;
}
/**
* @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
*/
constructor(configuration: Configuration) {
// 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;
const urlHash = window.location.hash;
const urlContainsHash = this.urlContainsHash(urlHash);
// On the server 302 - Redirect, handle this
if (!this.config.framework.isAngular) {
if (urlContainsHash) {
this.handleAuthenticationResponse(urlHash);
}
}
}
//#region Redirect Callbacks
/**
* @hidden
* @ignore
* Set the callback functions for the redirect flow to send back the success or error object.
* @param {@link (tokenReceivedCallback:type)} successCallback - Callback which contains the AuthResponse object, containing data from the server.
* @param {@link (errorReceivedCallback:type)} errorCallback - Callback which contains a AuthError object, containing error data from either the server
* or the library, depending on the origin of the error.
*/
handleRedirectCallback(tokenReceivedCallback: tokenReceivedCallback, errorReceivedCallback: errorReceivedCallback): void;
handleRedirectCallback(authCallback: authResponseCallback): void;
handleRedirectCallback(authOrTokenCallback: authResponseCallback | tokenReceivedCallback, errorReceivedCallback?: errorReceivedCallback): void {
if (!authOrTokenCallback) {
this.redirectCallbacksSet = false;
throw ClientConfigurationError.createInvalidCallbackObjectError(authOrTokenCallback);
}
// Set callbacks
if (errorReceivedCallback) {
this.tokenReceivedCallback = authOrTokenCallback as tokenReceivedCallback;
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 as authResponseCallback;
}
this.redirectCallbacksSet = true;
// On the server 302 - Redirect, handle this
if (!this.config.framework.isAngular) {
const cachedHash = this.cacheStorage.getItem(Constants.urlHash);
if (cachedHash) {
this.processCallBack(cachedHash, null);
}
}
}
private authResponseHandler(interactionType: InteractionType, response: AuthResponse, resolve?: any) : void {
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();
}
}
private authErrorHandler(interactionType: InteractionType, authErr: AuthError, response: AuthResponse, reject?: any) : void {
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)}
*/
loginRedirect(request?: AuthenticationParameters): void {
// 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
*/
acquireTokenRedirect(request: AuthenticationParameters): void {
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
*/
loginPopup(request?: AuthenticationParameters): Promise<AuthResponse> {
return new Promise<AuthResponse>((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
*/
acquireTokenPopup(request: AuthenticationParameters): Promise<AuthResponse> {
if (!request) {
throw ClientConfigurationError.createEmptyRequestError();
}
return new Promise<AuthResponse>((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
*/
private acquireTokenInteractive(interactionType: InteractionType, isLoginCall: boolean, request?: AuthenticationParameters, resolve?: any, reject?: any): void {
// If already in progress, do not proceed
if (this.loginInProgress || this.acquireTokenInProgress) {
const thrownError = this.loginInProgress ? ClientAuthError.createLoginInProgressError() : ClientAuthError.createAcquireTokenInProgressError();
const 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
const scopes: Array<string> = 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
const account: 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
let 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 ");
let tokenRequest: AuthenticationParameters = this.buildIDTokenRequest(request);
this.silentLogin = true;
this.acquireTokenSilent(tokenRequest).then(response => {
this.silentLogin = false;
this.logger.info("Unified cache call is successful");
this.authResponseHandler(interactionType, response, resolve);
return;
}, (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
*
*/
private acquireTokenHelper(account: Account, interactionType: InteractionType, isLoginCall: boolean, request?: AuthenticationParameters, scopes?: Array<string>, resolve?: any, reject?: any): void {
// Track the acquireToken progress
if (isLoginCall) {
this.loginInProgress = true;
} else {
this.acquireTokenInProgress = true;
}
const scope = scopes ? scopes.join(" ").toLowerCase() : this.clientId.toLowerCase();
let serverAuthenticationRequest: ServerRequestParameters;
const acquireTokenAuthority = (!isLoginCall && request && request.authority) ? AuthorityFactory.CreateInstance(request.authority, this.config.auth.validateAuthority) : this.authorityInstance;
let popUpWindow: Window;
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(() => {
// On Fulfillment
const responseType: string = isLoginCall ? ResponseTypes.id_token : this.getTokenType(account, scopes, false);
let loginStartPage: string;
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
let 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((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
*
*/
@resolveTokenOnlyIfOutOfIframe
acquireTokenSilent(request: AuthenticationParameters): Promise<AuthResponse> {
if (!request) {
throw ClientConfigurationError.createEmptyRequestError();
}
return new Promise<AuthResponse>((resolve, reject) => {
// Validate and filter scopes (the validate function will throw if validation fails)
this.validateInputScope(request.scopes, true);
const scope = request.scopes.join(" ").toLowerCase();
// if the developer passes an account, give that account the priority
const account: Account = request.account || this.getAccount();
// extract if there is an adalIdToken stashed in the cache
const 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());
}
const responseType = this.getTokenType(account, request.scopes, true);
let 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
const adalIdTokenObject = TokenUtils.extractIdToken(adalIdToken);
this.logger.verbose("ADAL's idToken exists. Extracting login information from ADAL's idToken ");
serverAuthenticationRequest.populateQueryParams(account, null, adalIdTokenObject);
}
const userContainedClaims = request.claimsRequest || serverAuthenticationRequest.claimsValue;
let authErr: AuthError;
let 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 {
let logMessage;
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(() => {
// 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((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
*/
private openWindow(urlNavigate: string, title: string, interval: number, instance: this, resolve?: Function, reject?: Function): Window {
// Generate a popup window
var popupWindow: Window;
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);
const pollTimer = window.setInterval(() => {
// 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 {
const 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 (let 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
*/
private openPopup(urlNavigate: string, title: string, popUpWidth: number, popUpHeight: number) {
try {
/**
* adding winLeft and winTop to account for dual monitor
* using screenLeft and screenTop for IE8 and earlier
*/
const winLeft = window.screenLeft ? window.screenLeft : window.screenX;
const 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
*/
const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
const height = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
const left = ((width / 2) - (popUpWidth / 2)) + winLeft;
const top = ((height / 2) - (popUpHeight / 2)) + winTop;
// open the window
const popupWindow = window.open(urlNavigate, title, "width=" + popUpWidth + ", height=" + popUpHeight + ", top=" + top + ", 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
*/
public isInIframe() {
return window.parent !== window;
}
/**
* @hidden
* Returns whether parent window exists and has msal
*/
private parentIsMsal() {
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
*/
private loadIframeTimeout(urlNavigate: string, frameName: string, scope: string): void {
//set iframe session to pending
const 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(() => {
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
*/
private loadFrame(urlNavigate: string, frameName: string): void {
// This trick overcomes iframe navigation in IE
// IE does not load the page consistently in iframe
this.logger.info("LoadFrame: " + frameName);
const frameCheck = frameName;
setTimeout(() => {
const 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
*/
private addHiddenIFrame(iframeId: string): HTMLIFrameElement {
if (typeof iframeId === "undefined") {
return null;
}
this.logger.info("Add msal frame to document:" + iframeId);
let adalFrame = document.getElementById(iframeId) as HTMLIFrameElement;
if (!adalFrame) {
if (document.createElement &&
document.documentElement &&
(window.navigator.userAgent.indexOf("MSIE 5.0") === -1)) {
const 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) as HTMLIFrameElement);
} 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
*/
private navigateWindow(urlNavigate: string, popupWindow?: Window) {
// Navigate if valid URL
if (urlNavigate && !StringUtils.isEmpty(urlNavigate)) {
let navigateWindow: Window = popupWindow ? popupWindow : window;
let logMessage: string = 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
*/
private registerCallback(expectedState: string, scope: string, resolve: Function, reject: Function): void {
// 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] =
(response: AuthResponse, error: AuthError) => {
// reset active renewals
window.activeRenewals[scope] = null;
// for all promiseMappedtoRenewStates for a given 'state' - call the reject/resolve with error/token respectively
for (let 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`.
*/
logout(): void {
this.clearCache();
this.account = null;
let logout = "";
if (this.getPostLogoutRedirectUri()) {
logout = "post_logout_redirect_uri=" + encodeURIComponent(this.getPostLogoutRedirectUri());
}
this.authorityInstance.resolveEndpointsAsync().then(authority => {
const 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
*/
protected clearCache(): void {
window.renewStates = [];
const accessTokenItems = this.cacheStorage.getAllAccessTokens(Constants.clientId, Constants.homeAccountIdentifier);
for (let 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
*/
protected clearCacheForScope(accessToken: string) {
const accessTokenItems = this.cacheStorage.getAllAccessTokens(Constants.clientId, Constants.homeAccountIdentifier);
for (let i = 0; i < accessTokenItems.length; i++) {
let 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.
*/
isCallback(hash: string): boolean {
this.logger.info("isCallback will be deprecated in favor of urlContainsHash in MSAL.js v2.0.");
return this.urlContainsHash(hash);
}
private urlContainsHash(urlString: string): boolean {
const 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.
*/
private processCallBack(hash: string, stateInfo: ResponseStateInfo, parentCallback?: Function): void {
this.logger.info("Processing the callback from redirect response");
// get the state info from the hash
if (!stateInfo) {
stateInfo = this.getResponseState(hash);
}
let response : AuthResponse;
let authErr : AuthError;
// 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();
const accountState: string = 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.
*/
private handleAuthenticationResponse(hash: string): void {
// retrieve the hash
if (hash == null) {
hash = window.location.hash;
}
let self = null;
let isPopup: boolean = false;
let 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
const stateInfo = self.getResponseState(hash);
let tokenResponseCallback: (response: AuthResponse, error: AuthError) => void = 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 (let i = 0; i < window.opener.openedWindows.length; i++) {
window.opener.openedWindows[i].close();
}
}
}
/**
* @hidden
* Returns deserialized portion of URL hash
* @param hash
*/
private deserializeHash(urlFragment: string) {
let hash = UrlUtils.getHashFromUrl(urlFragment);
return CryptoUtils.deserialize(hash);
}
/**
* @hidden
* Creates a stateInfo object from the URL fragment and returns it.
* @param {string} hash - Hash passed from redirect page
* @returns {TokenResponse} an object created from the redirect response from AAD comprising of the keys - parameters, requestType, stateMatch, stateResponse and valid.
* @ignore
*/
protected getResponseState(hash: string): ResponseStateInfo {
const parameters = this.deserializeHash(hash);
let stateResponse: ResponseStateInfo;
if (!parameters) {
throw AuthError.createUnexpectedError("Hash was not parsed correctly.");
}
if (parameters.hasOwnProperty("state")) {
stateResponse = {
requestType: Constants.unknown,
state: parameters.state,
stateMatch: false
};
} else {
throw AuthError.createUnexpectedError("Hash does not contain state.");
}
// async calls can fire iframe and login request at the same time if developer does not use the API as expected
// incoming callback needs to be looked up to find the request type
// loginRedirect
if (stateResponse.state === this.cacheStorage.getItem(Constants.stateLogin, this.inCookie) || stateResponse.state === t