adal-angular
Version:
Windows Azure Active Directory Client Library for js
1,116 lines (978 loc) • 73.9 kB
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: adal.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: adal.js</h1>
<section>
<article>
<pre class="prettyprint source"><code>//----------------------------------------------------------------------
// AdalJS v1.0.15
// @preserve Copyright (c) Microsoft Open Technologies, Inc.
// All Rights Reserved
// Apache License 2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//id
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//----------------------------------------------------------------------
var AuthenticationContext = (function () {
'use strict';
/**
* Configuration options for Authentication Context.
* @class config
* @property {string} tenant - Your target tenant.
* @property {string} clientId - Client ID assigned to your app by Azure Active Directory.
* @property {string} redirectUri - Endpoint at which you expect to receive tokens.Defaults to `window.location.href`.
* @property {string} instance - Azure Active Directory Instance.Defaults to `https://login.microsoftonline.com/`.
* @property {Array} endpoints - Collection of {Endpoint-ResourceId} used for automatically attaching tokens in webApi calls.
* @property {Boolean} popUp - Set this to true to enable login in a popup winodow instead of a full redirect.Defaults to `false`.
* @property {string} localLoginUrl - Set this to redirect the user to a custom login page.
* @property {function} displayCall - User defined function of handling the navigation to Azure AD authorization endpoint in case of login. Defaults to 'null'.
* @property {string} postLogoutRedirectUri - Redirects the user to postLogoutRedirectUri after logout. Defaults is 'redirectUri'.
* @property {string} cacheLocation - Sets browser storage to either 'localStorage' or sessionStorage'. Defaults to 'sessionStorage'.
* @property {Array.<string>} anonymousEndpoints Array of keywords or URI's. Adal will not attach a token to outgoing requests that have these keywords or uri. Defaults to 'null'.
* @property {number} expireOffsetSeconds If the cached token is about to be expired in the expireOffsetSeconds (in seconds), Adal will renew the token instead of using the cached token. Defaults to 300 seconds.
* @property {string} correlationId Unique identifier used to map the request with the response. Defaults to RFC4122 version 4 guid (128 bits).
* @property {number} loadFrameTimeout The number of milliseconds of inactivity before a token renewal response from AAD should be considered timed out.
*/
/**
* Creates a new AuthenticationContext object.
* @constructor
* @param {config} config Configuration options for AuthenticationContext
*/
AuthenticationContext = function (config) {
/**
* Enum for request type
* @enum {string}
*/
this.REQUEST_TYPE = {
LOGIN: 'LOGIN',
RENEW_TOKEN: 'RENEW_TOKEN',
UNKNOWN: 'UNKNOWN'
};
/**
* Enum for storage constants
* @enum {string}
*/
this.CONSTANTS = {
ACCESS_TOKEN: 'access_token',
EXPIRES_IN: 'expires_in',
ID_TOKEN: 'id_token',
ERROR_DESCRIPTION: 'error_description',
SESSION_STATE: 'session_state',
STORAGE: {
TOKEN_KEYS: 'adal.token.keys',
ACCESS_TOKEN_KEY: 'adal.access.token.key',
EXPIRATION_KEY: 'adal.expiration.key',
STATE_LOGIN: 'adal.state.login',
STATE_RENEW: 'adal.state.renew',
NONCE_IDTOKEN: 'adal.nonce.idtoken',
SESSION_STATE: 'adal.session.state',
USERNAME: 'adal.username',
IDTOKEN: 'adal.idtoken',
ERROR: 'adal.error',
ERROR_DESCRIPTION: 'adal.error.description',
LOGIN_REQUEST: 'adal.login.request',
LOGIN_ERROR: 'adal.login.error',
RENEW_STATUS: 'adal.token.renew.status'
},
RESOURCE_DELIMETER: '|',
LOADFRAME_TIMEOUT: 6000,
TOKEN_RENEW_STATUS_CANCELED: 'Canceled',
TOKEN_RENEW_STATUS_COMPLETED: 'Completed',
TOKEN_RENEW_STATUS_IN_PROGRESS: 'In Progress',
LOGGING_LEVEL: {
ERROR: 0,
WARN: 1,
INFO: 2,
VERBOSE: 3
},
LEVEL_STRING_MAP: {
0: 'ERROR:',
1: 'WARNING:',
2: 'INFO:',
3: 'VERBOSE:'
},
POPUP_WIDTH: 483,
POPUP_HEIGHT: 600
};
if (AuthenticationContext.prototype._singletonInstance) {
return AuthenticationContext.prototype._singletonInstance;
}
AuthenticationContext.prototype._singletonInstance = this;
// public
this.instance = 'https://login.microsoftonline.com/';
this.config = {};
this.callback = null;
this.popUp = false;
this.isAngular = false;
// private
this._user = null;
this._activeRenewals = {};
this._loginInProgress = false;
this._acquireTokenInProgress = false;
window.renewStates = [];
window.callBackMappedToRenewStates = {};
window.callBacksMappedToRenewStates = {};
// validate before constructor assignments
if (config.displayCall && typeof config.displayCall !== 'function') {
throw new Error('displayCall is not a function');
}
if (!config.clientId) {
throw new Error('clientId is required');
}
this.config = this._cloneConfig(config);
if (this.config.navigateToLoginRequestUrl === undefined)
this.config.navigateToLoginRequestUrl = true;
if (this.config.popUp)
this.popUp = true;
if (this.config.callback && typeof this.config.callback === 'function')
this.callback = this.config.callback;
if (this.config.instance) {
this.instance = this.config.instance;
}
// App can request idtoken for itself using clientid as resource
if (!this.config.loginResource) {
this.config.loginResource = this.config.clientId;
}
// redirect and logout_redirect are set to current location by default
if (!this.config.redirectUri) {
// strip off query parameters or hashes from the redirect uri as AAD does not allow those.
this.config.redirectUri = window.location.href.split("?")[0].split("#")[0];
}
if (!this.config.postLogoutRedirectUri) {
// strip off query parameters or hashes from the post logout redirect uri as AAD does not allow those.
this.config.postLogoutRedirectUri = window.location.href.split("?")[0].split("#")[0];
}
if (!this.config.anonymousEndpoints) {
this.config.anonymousEndpoints = [];
}
if (this.config.isAngular) {
this.isAngular = this.config.isAngular;
}
if (this.config.loadFrameTimeout) {
this.CONSTANTS.LOADFRAME_TIMEOUT = this.config.loadFrameTimeout;
}
};
if (typeof window !== 'undefined') {
window.Logging = {
level: 0,
log: function (message) { }
};
}
/**
* Initiates the login process by redirecting the user to Azure AD authorization endpoint.
*/
AuthenticationContext.prototype.login = function (loginStartPage) {
// Token is not present and user needs to login
if (this._loginInProgress) {
this.info("Login in progress");
return;
}
var expectedState = this._guid();
this.config.state = expectedState;
this._idTokenNonce = this._guid();
this.verbose('Expected state: ' + expectedState + ' startPage:' + window.location.href);
this._saveItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST, loginStartPage || window.location.href);
this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, '');
this._saveItem(this.CONSTANTS.STORAGE.STATE_LOGIN, expectedState);
this._saveItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN, this._idTokenNonce);
this._saveItem(this.CONSTANTS.STORAGE.ERROR, '');
this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, '');
var urlNavigate = this._getNavigateUrl('id_token', null) + '&nonce=' + encodeURIComponent(this._idTokenNonce);
this._loginInProgress = true;
if (this.config.displayCall) {
// User defined way of handling the navigation
this.config.displayCall(urlNavigate);
}
else if (this.popUp) {
this._loginPopup(urlNavigate);
}
else {
this.promptUser(urlNavigate);
}
};
/**
* Configures popup window for login.
* @ignore
*/
AuthenticationContext.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 = ((height / 2) - (popUpHeight / 2)) + winTop;
var popupWindow = window.open(urlNavigate, title, 'width=' + popUpWidth + ', height=' + popUpHeight + ', top=' + top + ', left=' + left);
if (popupWindow.focus) {
popupWindow.focus();
}
return popupWindow;
} catch (e) {
this.warn('Error opening popup, ' + e.message);
this._loginInProgress = false;
return null;
}
}
/**
* After authorization, the user will be sent to your specified redirect_uri with the user's bearer token
* attached to the URI fragment as an id_token field. It closes popup window after redirection.
* @ignore
*/
AuthenticationContext.prototype._loginPopup = function (urlNavigate, resource, callback) {
var popupWindow = this._openPopup(urlNavigate, "login", this.CONSTANTS.POPUP_WIDTH, this.CONSTANTS.POPUP_HEIGHT);
var loginCallback = callback || this.callback;
if (popupWindow == null) {
this.warn('Popup Window is null. This can happen if you are using IE');
this._saveItem(this.CONSTANTS.STORAGE.ERROR, 'Error opening popup');
this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, 'Popup Window is null. This can happen if you are using IE');
this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, 'Popup Window is null. This can happen if you are using IE');
if (resource && this._activeRenewals[resource]) {
this._activeRenewals[resource] = null;
}
this._loginInProgress = false;
this._acquireTokenInProgress = false;
if (loginCallback)
loginCallback(this._getItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION), null, this._getItem(this.CONSTANTS.STORAGE.ERROR));
return;
}
if (this.config.redirectUri.indexOf('#') != -1)
var registeredRedirectUri = this.config.redirectUri.split("#")[0];
else
var registeredRedirectUri = this.config.redirectUri;
var that = this;
var pollTimer = window.setInterval(function () {
if (!popupWindow || popupWindow.closed || popupWindow.closed === undefined) {
that._loginInProgress = false;
that._acquireTokenInProgress = false;
if (resource && that._activeRenewals[resource]) {
that._activeRenewals[resource] = null;
}
window.clearInterval(pollTimer);
}
try {
if (popupWindow.location.href.indexOf(registeredRedirectUri) != -1) {
if (that.isAngular) {
that._onPopUpHashChanged(popupWindow.location.hash);
}
else {
that.handleWindowCallback(popupWindow.location.hash);
}
window.clearInterval(pollTimer);
that._loginInProgress = false;
that._acquireTokenInProgress = false;
that.info("Closing popup window");
popupWindow.close();
}
} catch (e) {
}
}, 1);
};
AuthenticationContext.prototype._onPopUpHashChanged = function (hash) {
// Custom Event is not supported in IE, below IIFE will polyfill the CustomEvent() constructor functionality in Internet Explorer 9 and higher
(function () {
if (typeof window.CustomEvent === "function") {
return false;
}
function CustomEvent(event, params) {
params = params || { bubbles: false, cancelable: false, detail: undefined };
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
return evt;
}
CustomEvent.prototype = window.Event.prototype;
window.CustomEvent = CustomEvent;
})();
var evt = new CustomEvent('adal:popUpHashChanged', { detail: hash });
window.dispatchEvent(evt);
};
AuthenticationContext.prototype.loginInProgress = function () {
return this._loginInProgress;
};
/**
* Checks for the resource in the cache. By default, cache location is Session Storage
* @ignore
* @returns {Boolean} 'true' if login is in progress, else returns 'false'.
*/
AuthenticationContext.prototype._hasResource = function (key) {
var keys = this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS);
return keys && !this._isEmpty(keys) && (keys.indexOf(key + this.CONSTANTS.RESOURCE_DELIMETER) > -1);
};
/**
* Gets token for the specified resource from the cache.
* @param {string} resource A URI that identifies the resource for which the token is requested.
* @returns {string} token if if it exists and not expired, otherwise null.
*/
AuthenticationContext.prototype.getCachedToken = function (resource) {
if (!this._hasResource(resource)) {
return null;
}
var token = this._getItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource);
var expiry = this._getItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource);
// If expiration is within offset, it will force renew
var offset = this.config.expireOffsetSeconds || 300;
if (expiry && (expiry > this._now() + offset)) {
return token;
} else {
this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource, '');
this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource, 0);
return null;
}
};
/**
* User information from idtoken.
* @class User
* @property {string} userName - username assigned from upn or email.
* @property {object} profile - properties parsed from idtoken.
*/
/**
* If user object exists, returns it. Else creates a new user object by decoding id_token from the cache.
* @returns {User} user object
*/
AuthenticationContext.prototype.getCachedUser = function () {
if (this._user) {
return this._user;
}
var idtoken = this._getItem(this.CONSTANTS.STORAGE.IDTOKEN);
this._user = this._createUser(idtoken);
return this._user;
};
/**
* Adds the passed callback to the array of callbacks for the specified resource and puts the array on the window object.
* @param {string} resource A URI that identifies the resource for which the token is requested.
* @param {string} expectedState A unique identifier (guid).
* @param {tokenCallback} callback - The callback provided by the caller. It will be called with token or error.
*/
AuthenticationContext.prototype.registerCallback = function (expectedState, resource, callback) {
this._activeRenewals[resource] = expectedState;
if (!window.callBacksMappedToRenewStates[expectedState]) {
window.callBacksMappedToRenewStates[expectedState] = [];
}
var self = this;
window.callBacksMappedToRenewStates[expectedState].push(callback);
if (!window.callBackMappedToRenewStates[expectedState]) {
window.callBackMappedToRenewStates[expectedState] = function (errorDesc, token, error) {
self._activeRenewals[resource] = null;
for (var i = 0; i < window.callBacksMappedToRenewStates[expectedState].length; ++i) {
try {
window.callBacksMappedToRenewStates[expectedState][i](errorDesc, token, error);
}
catch (error) {
self.warn(error);
}
}
window.callBacksMappedToRenewStates[expectedState] = null;
window.callBackMappedToRenewStates[expectedState] = null;
};
}
};
// var errorResponse = {error:'', error_description:''};
// var token = 'string token';
// callback(errorResponse, token)
// with callback
/**
* Acquires access token with hidden iframe
* @ignore
*/
AuthenticationContext.prototype._renewToken = function (resource, callback) {
// use iframe to try refresh token
// use given resource to create new authz url
this.info('renewToken is called for resource:' + resource);
var frameHandle = this._addAdalFrame('adalRenewFrame' + resource);
var expectedState = this._guid() + '|' + resource;
this.config.state = expectedState;
// renew happens in iframe, so it keeps javascript context
window.renewStates.push(expectedState);
this.verbose('Renew token Expected state: ' + expectedState);
var urlNavigate = this._getNavigateUrl('token', resource) + '&prompt=none';
urlNavigate = this._addHintParameters(urlNavigate);
this.registerCallback(expectedState, resource, callback);
this.verbose('Navigate to:' + urlNavigate);
frameHandle.src = 'about:blank';
this._loadFrameTimeout(urlNavigate, 'adalRenewFrame' + resource, resource);
};
/**
* Renews idtoken for app's own backend when resource is clientId and calls the callback with token/error
* @ignore
*/
AuthenticationContext.prototype._renewIdToken = function (callback) {
// use iframe to try refresh token
this.info('renewIdToken is called');
var frameHandle = this._addAdalFrame('adalIdTokenFrame');
var expectedState = this._guid() + '|' + this.config.clientId;
this._idTokenNonce = this._guid();
this._saveItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN, this._idTokenNonce);
this.config.state = expectedState;
// renew happens in iframe, so it keeps javascript context
window.renewStates.push(expectedState);
this.verbose('Renew Idtoken Expected state: ' + expectedState);
var urlNavigate = this._getNavigateUrl('id_token', null) + '&prompt=none';
urlNavigate = this._addHintParameters(urlNavigate);
urlNavigate += '&nonce=' + encodeURIComponent(this._idTokenNonce);
this.registerCallback(expectedState, this.config.clientId, callback);
this.idTokenNonce = null;
this.verbose('Navigate to:' + urlNavigate);
frameHandle.src = 'about:blank';
this._loadFrameTimeout(urlNavigate, 'adalIdTokenFrame', this.config.clientId);
};
/**
* Checks if the authorization endpoint URL contains query string parameters
* @ignore
*/
AuthenticationContext.prototype._urlContainsQueryStringParameter = function (name, url) {
// regex to detect pattern of a ? or & followed by the name parameter and an equals character
var regex = new RegExp("[\\?&]" + name + "=");
return regex.test(url);
}
// 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
*/
AuthenticationContext.prototype._loadFrameTimeout = function (urlNavigation, frameName, resource) {
//set iframe session to pending
this.verbose('Set loading state to pending for: ' + resource);
this._saveItem(this.CONSTANTS.STORAGE.RENEW_STATUS + resource, this.CONSTANTS.TOKEN_RENEW_STATUS_IN_PROGRESS);
this._loadFrame(urlNavigation, frameName);
var self = this;
setTimeout(function () {
if (self._getItem(self.CONSTANTS.STORAGE.RENEW_STATUS + resource) === self.CONSTANTS.TOKEN_RENEW_STATUS_IN_PROGRESS) {
// fail the iframe session if it's in pending state
self.verbose('Loading frame has timed out after: ' + (self.CONSTANTS.LOADFRAME_TIMEOUT / 1000) + ' seconds for resource ' + resource);
var expectedState = self._activeRenewals[resource];
if (expectedState && window.callBackMappedToRenewStates[expectedState]) {
window.callBackMappedToRenewStates[expectedState]('Token renewal operation failed due to timeout', null, 'Token Renewal Failed');
}
self._saveItem(self.CONSTANTS.STORAGE.RENEW_STATUS + resource, self.CONSTANTS.TOKEN_RENEW_STATUS_CANCELED);
}
}, self.CONSTANTS.LOADFRAME_TIMEOUT);
}
/**
* Loads iframe with authorization endpoint URL
* @ignore
*/
AuthenticationContext.prototype._loadFrame = function (urlNavigate, frameName) {
// This trick overcomes iframe navigation in IE
// IE does not load the page consistently in iframe
var self = this;
self.info('LoadFrame: ' + frameName);
var frameCheck = frameName;
setTimeout(function () {
var frameHandle = self._addAdalFrame(frameCheck);
if (frameHandle.src === '' || frameHandle.src === 'about:blank') {
frameHandle.src = urlNavigate;
self._loadFrame(urlNavigate, frameCheck);
}
}, 500);
};
/**
* @callback tokenCallback
* @param {string} error_description error description returned from AAD if token request fails.
* @param {string} token token returned from AAD if token request is successful.
* @param {string} error error message returned from AAD if token request fails.
*/
/**
* Acquires token from the cache if it is not expired. Otherwise sends request to AAD to obtain a new token.
* @param {string} resource ResourceUri identifying the target resource
* @param {tokenCallback} callback - The callback provided by the caller. It will be called with token or error.
*/
AuthenticationContext.prototype.acquireToken = function (resource, callback) {
if (this._isEmpty(resource)) {
this.warn('resource is required');
callback('resource is required', null, 'resource is required');
return;
}
var token = this.getCachedToken(resource);
if (token) {
this.info('Token is already in cache for resource:' + resource);
callback(null, token, null);
return;
}
if (!this._user) {
this.warn('User login is required');
callback('User login is required', null, 'login required');
return;
}
// refresh attept with iframe
//Already renewing for this resource, callback when we get the token.
if (this._activeRenewals[resource]) {
//Active renewals contains the state for each renewal.
this.registerCallback(this._activeRenewals[resource], resource, callback);
}
else {
if (resource === this.config.clientId) {
// App uses idtoken to send to api endpoints
// Default resource is tracked as clientid to store this token
this.verbose('renewing idtoken');
this._renewIdToken(callback);
} else {
this._renewToken(resource, callback);
}
}
};
/**
* Acquires token (interactive flow using a popUp window) by sending request to AAD to obtain a new token.
* @param {string} resource ResourceUri identifying the target resource
* @param {string} extraQueryParameters extraQueryParameters to add to the authentication request
* @param {tokenCallback} callback - The callback provided by the caller. It will be called with token or error.
*/
AuthenticationContext.prototype.acquireTokenPopup = function (resource, extraQueryParameters, claims, callback) {
if (this._isEmpty(resource)) {
this.warn('resource is required');
callback('resource is required', null, 'resource is required');
return;
}
if (!this._user) {
this.warn('User login is required');
callback('User login is required', null, 'login required');
return;
}
if (this._acquireTokenInProgress) {
this.warn("Acquire token interactive is already in progress")
callback("Acquire token interactive is already in progress", null, "Acquire token interactive is already in progress");
return;
}
var expectedState = this._guid() + '|' + resource;
this.config.state = expectedState;
window.renewStates.push(expectedState);
this.verbose('Renew token Expected state: ' + expectedState);
var urlNavigate = this._getNavigateUrl('token', resource) + '&prompt=select_account';
if (extraQueryParameters) {
urlNavigate += encodeURIComponent(extraQueryParameters);
}
if (claims && (urlNavigate.indexOf("&claims") === -1)) {
urlNavigate += '&claims=' + encodeURIComponent(claims);
}
else if (claims && (urlNavigate.indexOf("&claims") !== -1)) {
throw new Error('Claims cannot be passed as an extraQueryParameter');
}
urlNavigate = this._addHintParameters(urlNavigate);
this._acquireTokenInProgress = true;
this.info('acquireToken interactive is called for the resource ' + resource);
this.registerCallback(expectedState, resource, callback);
this._loginPopup(urlNavigate, resource, callback);
};
/**
* Acquires token (interactive flow using a redirect) by sending request to AAD to obtain a new token. In this case the callback passed in the Authentication
* request constructor will be called.
* @param {string} resource ResourceUri identifying the target resource
* @param {string} extraQueryParameters extraQueryParameters to add to the authentication request
*/
AuthenticationContext.prototype.acquireTokenRedirect = function (resource, extraQueryParameters, claims) {
if (this._isEmpty(resource)) {
this.warn('resource is required');
callback('resource is required', null, 'resource is required');
return;
}
if (!this._user) {
this.warn('User login is required');
callback('User login is required', null, 'login required');
return;
}
if (this._acquireTokenInProgress) {
this.warn("Acquire token interactive is already in progress")
callback("Acquire token interactive is already in progress", null, "Acquire token interactive is already in progress");
return;
}
var expectedState = this._guid() + '|' + resource;
this.config.state = expectedState;
window.renewStates.push(expectedState);
this.verbose('Renew token Expected state: ' + expectedState);
var urlNavigate = this._getNavigateUrl('token', resource) + '&prompt=select_account';
if (extraQueryParameters) {
urlNavigate += encodeURIComponent(extraQueryParameters);
}
if (claims && (urlNavigate.indexOf("&claims") === -1)) {
urlNavigate += '&claims=' + encodeURIComponent(claims);
}
else if (claims && (urlNavigate.indexOf("&claims") !== -1)) {
throw new Error('Claims cannot be passed as an extraQueryParameter');
}
urlNavigate = this._addHintParameters(urlNavigate);
this._acquireTokenInProgress = true;
this.info('acquireToken interactive is called for the resource ' + resource);
this._saveItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST, window.location.href);
this._saveItem(this.CONSTANTS.STORAGE.STATE_RENEW, expectedState);
this.promptUser(urlNavigate);
};
/**
* Redirects the browser to Azure AD authorization endpoint.
* @param {string} urlNavigate Url of the authorization endpoint.
*/
AuthenticationContext.prototype.promptUser = function (urlNavigate) {
if (urlNavigate) {
this.info('Navigate to:' + urlNavigate);
window.location.replace(urlNavigate);
} else {
this.info('Navigate url is empty');
}
};
/**
* Clears cache items.
*/
AuthenticationContext.prototype.clearCache = function () {
this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY, 0);
this._saveItem(this.CONSTANTS.STORAGE.SESSION_STATE, '');
this._saveItem(this.CONSTANTS.STORAGE.STATE_LOGIN, '');
window.renewStates = [];
this._saveItem(this.CONSTANTS.STORAGE.IDTOKEN, '');
this._saveItem(this.CONSTANTS.STORAGE.ERROR, '');
this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, '');
this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, '');
var keys = this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS);
if (!this._isEmpty(keys)) {
keys = keys.split(this.CONSTANTS.RESOURCE_DELIMETER);
for (var i = 0; i < keys.length; i++) {
this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + keys[i], '');
this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + keys[i], 0);
}
}
this._saveItem(this.CONSTANTS.STORAGE.TOKEN_KEYS, '');
};
/**
* Clears cache items for a given resource.
* @param {string} resource a URI that identifies the resource.
*/
AuthenticationContext.prototype.clearCacheForResource = function (resource) {
this._saveItem(this.CONSTANTS.STORAGE.STATE_RENEW, '');
this._saveItem(this.CONSTANTS.STORAGE.ERROR, '');
this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, '');
if (this._hasResource(resource)) {
this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource, '');
this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource, 0);
}
};
/**
* Redirects user to logout endpoint.
* After logout, it will redirect to postLogoutRedirectUri if added as a property on the config object.
*/
AuthenticationContext.prototype.logOut = function () {
this.clearCache();
this._user = null;
var urlNavigate;
if (this.config.logOutUri) {
urlNavigate = this.config.logOutUri;
} else {
var tenant = 'common';
var logout = '';
if (this.config.tenant) {
tenant = this.config.tenant;
}
if (this.config.postLogoutRedirectUri) {
logout = 'post_logout_redirect_uri=' + encodeURIComponent(this.config.postLogoutRedirectUri);
}
urlNavigate = this.instance + tenant + '/oauth2/logout?' + logout;
}
this.info('Logout navigate to: ' + urlNavigate);
this.promptUser(urlNavigate);
};
AuthenticationContext.prototype._isEmpty = function (str) {
return (typeof str === 'undefined' || !str || 0 === str.length);
};
/**
* @callback userCallback
* @param {string} error error message if user info is not available.
* @param {User} user user object retrieved from the cache.
*/
/**
* Calls the passed in callback with the user object or error message related to the user.
* @param {userCallback} callback - The callback provided by the caller. It will be called with user or error.
*/
AuthenticationContext.prototype.getUser = function (callback) {
// IDToken is first call
if (typeof callback !== 'function') {
throw new Error('callback is not a function');
}
// user in memory
if (this._user) {
callback(null, this._user);
return;
}
// frame is used to get idtoken
var idtoken = this._getItem(this.CONSTANTS.STORAGE.IDTOKEN);
if (!this._isEmpty(idtoken)) {
this.info('User exists in cache: ');
this._user = this._createUser(idtoken);
callback(null, this._user);
} else {
this.warn('User information is not available');
callback('User information is not available', null);
}
};
/**
* 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/organisations which when added skips the email based discovery process of the user.
* @ignore
*/
AuthenticationContext.prototype._addHintParameters = function (urlNavigate) {
// include hint params only if upn is present
if (this._user && this._user.profile && this._user.profile.hasOwnProperty('upn')) {
// don't add login_hint twice if user provided it in the extraQueryParameter value
if (!this._urlContainsQueryStringParameter("login_hint", urlNavigate)) {
// add login_hint
urlNavigate += '&login_hint=' + encodeURIComponent(this._user.profile.upn);
}
// don't add domain_hint twice if user provided it in the extraQueryParameter value
if (!this._urlContainsQueryStringParameter("domain_hint", urlNavigate) && this._user.profile.upn.indexOf('@') > -1) {
var parts = this._user.profile.upn.split('@');
// local part can include @ in quotes. Sending last part handles that.
urlNavigate += '&domain_hint=' + encodeURIComponent(parts[parts.length - 1]);
}
}
return urlNavigate;
}
/**
* Creates a user object by decoding the id_token
* @ignore
*/
AuthenticationContext.prototype._createUser = function (idToken) {
var user = null;
var parsedJson = this._extractIdToken(idToken);
if (parsedJson && parsedJson.hasOwnProperty('aud')) {
if (parsedJson.aud.toLowerCase() === this.config.clientId.toLowerCase()) {
user = {
userName: '',
profile: parsedJson
};
if (parsedJson.hasOwnProperty('upn')) {
user.userName = parsedJson.upn;
} else if (parsedJson.hasOwnProperty('email')) {
user.userName = parsedJson.email;
}
} else {
this.warn('IdToken has invalid aud field');
}
}
return user;
};
/**
* Returns the anchor part(#) of the URL
* @ignore
*/
AuthenticationContext.prototype._getHash = function (hash) {
if (hash.indexOf('#/') > -1) {
hash = hash.substring(hash.indexOf('#/') + 2);
} else if (hash.indexOf('#') > -1) {
hash = hash.substring(1);
}
return hash;
};
/**
* Checks if the URL fragment contains access token, id token or error_description.
* @param {string} hash - Hash passed from redirect page
* @returns {Boolean} true if response contains id_token, access_token or error, false otherwise.
*/
AuthenticationContext.prototype.isCallback = function (hash) {
hash = this._getHash(hash);
var parameters = this._deserialize(hash);
return (
parameters.hasOwnProperty(this.CONSTANTS.ERROR_DESCRIPTION) ||
parameters.hasOwnProperty(this.CONSTANTS.ACCESS_TOKEN) ||
parameters.hasOwnProperty(this.CONSTANTS.ID_TOKEN)
);
};
/**
* Gets login error
* @returns {string} error message related to login.
*/
AuthenticationContext.prototype.getLoginError = function () {
return this._getItem(this.CONSTANTS.STORAGE.LOGIN_ERROR);
};
/**
* Request info object created from the response received from AAD.
* @class RequestInfo
* @property {object} parameters - object comprising of fields such as id_token/error, session_state, state, e.t.c.
* @property {REQUEST_TYPE} requestType - either LOGIN, RENEW_TOKEN or UNKNOWN.
* @property {boolean} stateMatch - true if state is valid, false otherwise.
* @property {string} stateResponse - unique guid used to match the response with the request.
* @property {boolean} valid - true if requestType contains id_token, access_token or error, false otherwise.
*/
/**
* Creates a requestInfo object from the URL fragment and returns it.
* @returns {RequestInfo} an object created from the redirect response from AAD comprising of the keys - parameters, requestType, stateMatch, stateResponse and valid.
*/
AuthenticationContext.prototype.getRequestInfo = function (hash) {
hash = this._getHash(hash);
var parameters = this._deserialize(hash);
var requestInfo = {
valid: false,
parameters: {},
stateMatch: false,
stateResponse: '',
requestType: this.REQUEST_TYPE.UNKNOWN
};
if (parameters) {
requestInfo.parameters = parameters;
if (parameters.hasOwnProperty(this.CONSTANTS.ERROR_DESCRIPTION) ||
parameters.hasOwnProperty(this.CONSTANTS.ACCESS_TOKEN) ||
parameters.hasOwnProperty(this.CONSTANTS.ID_TOKEN)) {
requestInfo.valid = true;
// which call
var stateResponse = '';
if (parameters.hasOwnProperty('state')) {
this.verbose('State: ' + parameters.state);
stateResponse = parameters.state;
} else {
this.warn('No state returned');
return requestInfo;
}
requestInfo.stateResponse = stateResponse;
// 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
if (stateResponse === this._getItem(this.CONSTANTS.STORAGE.STATE_LOGIN)) {
requestInfo.requestType = this.REQUEST_TYPE.LOGIN;
requestInfo.stateMatch = true;
return requestInfo;
}
else if (stateResponse === this._getItem(this.CONSTANTS.STORAGE.STATE_RENEW)) {
requestInfo.requestType = this.REQUEST_TYPE.RENEW_TOKEN;
requestInfo.stateMatch = true;
return requestInfo;
}
// external api requests may have many renewtoken requests for different resource
if (!requestInfo.stateMatch && window.parent) {
var statesInParentContext = window.parent.renewStates;
for (var i = 0; i < statesInParentContext.length; i++) {
if (statesInParentContext[i] === requestInfo.stateResponse) {
requestInfo.requestType = this.REQUEST_TYPE.RENEW_TOKEN;
requestInfo.stateMatch = true;
break;
}
}
}
}
}
return requestInfo;
};
/**
* Extracts resource value from state.
* @ignore
*/
AuthenticationContext.prototype._getResourceFromState = function (state) {
if (state) {
var splitIndex = state.indexOf('|');
if (splitIndex > -1 && splitIndex + 1 < state.length) {
return state.substring(splitIndex + 1);
}
}
return '';
};
/**
* Saves token or error received in the response from AAD in the cache. In case of id_token, it also creates the user object.
*/
AuthenticationContext.prototype.saveTokenFromHash = function (requestInfo) {
this.info('State status:' + requestInfo.stateMatch + '; Request type:' + requestInfo.requestType);
this._saveItem(this.CONSTANTS.STORAGE.ERROR, '');
this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, '');
var resource = this._getResourceFromState(requestInfo.stateResponse);
// Record error
if (requestInfo.parameters.hasOwnProperty(this.CONSTANTS.ERROR_DESCRIPTION)) {
this.info('Error :' + requestInfo.parameters.error + '; Error description:' + requestInfo.parameters[this.CONSTANTS.ERROR_DESCRIPTION]);
this._saveItem(this.CONSTANTS.STORAGE.ERROR, requestInfo.parameters.error);
this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, requestInfo.parameters[this.CONSTANTS.ERROR_DESCRIPTION]);
if (requestInfo.requestType === this.REQUEST_TYPE.LOGIN) {
this._loginInProgress = false;
this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, requestInfo.parameters.error_description);
}
} else {
// It must verify the state from redirect
if (requestInfo.stateMatch) {
// record tokens to storage if exists
this.info('State is right');
if (requestInfo.parameters.hasOwnProperty(this.CONSTANTS.SESSION_STATE)) {
this._saveItem(this.CONSTANTS.STORAGE.SESSION_STATE, requestInfo.parameters[this.CONSTANTS.SESSION_STATE]);
}
var keys;
if (requestInfo.parameters.hasOwnProperty(this.CONSTANTS.ACCESS_TOKEN)) {
this.info('Fragment has access token');
if (!this._hasResource(resource)) {
keys = this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS) || '';
this._saveItem(this.CONSTANTS.STORAGE.TOKEN_KEYS, keys + resource + this.CONSTANTS.RESOURCE_DELIMETER);
}
// save token with related resource
this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource, requestInfo.parameters[this.CONSTANTS.ACCESS_TOKEN]);
this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource, this._expiresIn(requestInfo.parameters[this.CONSTANTS.EXPIRES_IN]));
}
if (requestInfo.parameters.hasOwnProperty(this.CONSTANTS.ID_TOKEN)) {
this.info('Fragment has id token');
this._loginInProgress = false;
this._user = this._createUser(requestInfo.parameters[this.CONSTANTS.ID_TOKEN]);
if (this._user && this._user.profile) {
if (this._user.profile.nonce !== this._getItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN)) {
this._user = null;
this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, 'Nonce is not same as ' + this._idTokenNonce);
} else {
this._saveItem(this.CONSTANTS.STORAGE.IDTOKEN, requestInfo.parameters[this.CONSTANTS.ID_TOKEN]);
// Save idtoken as access token for app itself
resource = this.config.loginResource ? this.config.loginResource : this.config.clientId;
if (!this._hasResource(resource)) {
keys = this._getItem(this.CONSTANTS.STORAGE.TOKEN_KEYS) || '';
this._saveItem(this.CONSTANTS.STORAGE.TOKEN_KEYS, keys + resource + this.CONSTANTS.RESOURCE_DELIMETER);
}
this._saveItem(this.CONSTANTS.STORAGE.ACCESS_TOKEN_KEY + resource, requestInfo.parameters[this.CONSTANTS.ID_TOKEN]);
this._saveItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + resource, this._user.profile.exp);
}
}
else {
this._saveItem(this.CONSTANTS.STORAGE.ERROR, 'invalid id_token');
this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, 'Invalid id_token. id_token: ' + requestInfo.parameters[this.CONSTANTS.ID_TOKEN]);
}
}
} else {
this._saveItem(this.CONSTANTS.STORAGE.ERROR, 'Invalid_state');
this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, 'Invalid_state. state: ' + requestInfo.stateResponse);
}
}
this._saveItem(this.CONSTANTS.STORAGE.RENEW_STATUS + resource, this.CONSTANTS.TOKEN_RENEW_STATUS_COMPLETED);
};
/**
* Gets resource for given endpoint if mapping is provided with config.
* @param {string} endpoint - The URI for which the resource Id is requested.
* @returns {string} resource for this API endpoint.
*/
AuthenticationContext.prototype.getResourceForEndpoint = function (endpoint) {
// if user specified list of anonymous endpoints, no need to send token to these endpoints, return null.
if (this.config && this.config.anonymousEndpoints) {
for (var i = 0; i < this.config.anonymousEndpoints.length; i++) {
if (endpoint.indexOf(this.config.anonymousEndpoints[i]) > -1) {
return null;
}
}
}
if (this.config && this.config.endpoints) {
for (var configEndpoint in this.config.endpoints) {
// configEndpoint is like /api/Todo requested endpoint can be /api/Todo/1
if (endpoint.indexOf(configEndpoint) > -1) {
return this.config.endpoints[configEndpoint];
}
}
}
// default resource will be clientid if nothing specified