adal-angular
Version:
Windows Azure Active Directory Client Library for js
1,162 lines (1,007 loc) • 82.6 kB
JavaScript
//----------------------------------------------------------------------
// AdalJS v1.0.18
// @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'
};
this.RESPONSE_TYPE = {
ID_TOKEN_TOKEN: 'id_token token',
TOKEN: 'token'
};
/**
* 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',
ERROR: 'error',
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',
ANGULAR_LOGIN_REQUEST: 'adal.angular.login.request'
},
RESOURCE_DELIMETER: '|',
CACHE_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;
this._renewStates = [];
this._callBackMappedToRenewStates = {};
this._callBacksMappedToRenewStates = {};
this._openedWindows = [];
this._requestType = this.REQUEST_TYPE.LOGIN;
window._adalInstance = this;
// 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 = {
piiLoggingEnabled: false,
level: 0,
log: function (message) { }
};
}
/**
* Initiates the login process by redirecting the user to Azure AD authorization endpoint.
*/
AuthenticationContext.prototype.login = function () {
if (this._loginInProgress) {
this.info("Login in progress");
return;
}
this._loginInProgress = true;
// Token is not present and user needs to login
var expectedState = this._guid();
this.config.state = expectedState;
this._idTokenNonce = this._guid();
var loginStartPage = this._getItem(this.CONSTANTS.STORAGE.ANGULAR_LOGIN_REQUEST);
if (!loginStartPage || loginStartPage === "") {
loginStartPage = window.location.href;
}
else {
this._saveItem(this.CONSTANTS.STORAGE.ANGULAR_LOGIN_REQUEST, "")
}
this.verbose('Expected state: ' + expectedState + ' startPage:' + loginStartPage);
this._saveItem(this.CONSTANTS.STORAGE.LOGIN_REQUEST, loginStartPage);
this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, '');
this._saveItem(this.CONSTANTS.STORAGE.STATE_LOGIN, expectedState, true);
this._saveItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN, this._idTokenNonce, true);
this._saveItem(this.CONSTANTS.STORAGE.ERROR, '');
this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, '');
var urlNavigate = this._getNavigateUrl('id_token', null) + '&nonce=' + encodeURIComponent(this._idTokenNonce);
if (this.config.displayCall) {
// User defined way of handling the navigation
this.config.displayCall(urlNavigate);
}
else if (this.popUp) {
this._saveItem(this.CONSTANTS.STORAGE.STATE_LOGIN, '');// so requestInfo does not match redirect case
this._renewStates.push(expectedState);
this.registerCallback(expectedState, this.config.clientId, this.callback);
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;
this._acquireTokenInProgress = false;
return null;
}
}
AuthenticationContext.prototype._handlePopupError = function (loginCallback, resource, error, errorDesc, loginError) {
this.warn(errorDesc);
this._saveItem(this.CONSTANTS.STORAGE.ERROR, error);
this._saveItem(this.CONSTANTS.STORAGE.ERROR_DESCRIPTION, errorDesc);
this._saveItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, loginError);
if (resource && this._activeRenewals[resource]) {
this._activeRenewals[resource] = null;
}
this._loginInProgress = false;
this._acquireTokenInProgress = false;
if (loginCallback) {
loginCallback(errorDesc, null, error);
}
}
/**
* 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) {
var error = 'Error opening popup';
var errorDesc = 'Popup Window is null. This can happen if you are using IE';
this._handlePopupError(loginCallback, resource, error, errorDesc, errorDesc);
return;
}
this._openedWindows.push(popupWindow);
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) {
var error = 'Popup Window closed';
var errorDesc = 'Popup Window closed by UI action/ Popup Window handle destroyed due to cross zone navigation in IE/Edge'
if (that.isAngular) {
that._broadcast('adal:popUpClosed', errorDesc + that.CONSTANTS.RESOURCE_DELIMETER + error);
}
that._handlePopupError(loginCallback, resource, error, errorDesc, errorDesc);
window.clearInterval(pollTimer);
return;
}
try {
var popUpWindowLocation = popupWindow.location;
if (encodeURI(popUpWindowLocation.href).indexOf(encodeURI(registeredRedirectUri)) != -1) {
if (that.isAngular) {
that._broadcast('adal:popUpHashChanged', popUpWindowLocation.hash);
}
else {
that.handleWindowCallback(popUpWindowLocation.hash);
}
window.clearInterval(pollTimer);
that._loginInProgress = false;
that._acquireTokenInProgress = false;
that.info("Closing popup window");
that._openedWindows = [];
popupWindow.close();
return;
}
} catch (e) {
}
}, 1);
};
AuthenticationContext.prototype._broadcast = function (eventName, data) {
// 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(eventName, { detail: data });
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 (!this._callBacksMappedToRenewStates[expectedState]) {
this._callBacksMappedToRenewStates[expectedState] = [];
}
var self = this;
this._callBacksMappedToRenewStates[expectedState].push(callback);
if (!this._callBackMappedToRenewStates[expectedState]) {
this._callBackMappedToRenewStates[expectedState] = function (errorDesc, token, error, tokenType) {
self._activeRenewals[resource] = null;
for (var i = 0; i < self._callBacksMappedToRenewStates[expectedState].length; ++i) {
try {
self._callBacksMappedToRenewStates[expectedState][i](errorDesc, token, error, tokenType);
}
catch (error) {
self.warn(error);
}
}
self._callBacksMappedToRenewStates[expectedState] = null;
self._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, responseType) {
// use iframe to try to renew 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
this._renewStates.push(expectedState);
this.verbose('Renew token Expected state: ' + expectedState);
// remove the existing prompt=... query parameter and add prompt=none
responseType = responseType || 'token';
var urlNavigate = this._urlRemoveQueryStringParameter(this._getNavigateUrl(responseType, resource), 'prompt');
if (responseType === this.RESPONSE_TYPE.ID_TOKEN_TOKEN) {
this._idTokenNonce = this._guid();
this._saveItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN, this._idTokenNonce, true);
urlNavigate += '&nonce=' + encodeURIComponent(this._idTokenNonce);
}
urlNavigate = urlNavigate + '&prompt=none';
urlNavigate = this._addHintParameters(urlNavigate);
this.registerCallback(expectedState, resource, callback);
this.verbosePii('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, responseType) {
// use iframe to try to renew 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, true);
this.config.state = expectedState;
// renew happens in iframe, so it keeps javascript context
this._renewStates.push(expectedState);
this.verbose('Renew Idtoken Expected state: ' + expectedState);
// remove the existing prompt=... query parameter and add prompt=none
var resource = responseType === null || typeof (responseType) === "undefined" ? null : this.config.clientId;
var responseType = responseType || 'id_token';
var urlNavigate = this._urlRemoveQueryStringParameter(this._getNavigateUrl(responseType, resource), 'prompt');
urlNavigate = urlNavigate + '&prompt=none';
urlNavigate = this._addHintParameters(urlNavigate);
urlNavigate += '&nonce=' + encodeURIComponent(this._idTokenNonce);
this.registerCallback(expectedState, this.config.clientId, callback);
this.verbosePii('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);
}
/**
* Removes the query string parameter from the authorization endpoint URL if it exists
* @ignore
*/
AuthenticationContext.prototype._urlRemoveQueryStringParameter = function (url, name) {
// we remove &name=value, name=value& and name=value
// &name=value
var regex = new RegExp('(\\&' + name + '=)[^\&]+');
url = url.replace(regex, '');
// name=value&
regex = new RegExp('(' + name + '=)[^\&]+&');
url = url.replace(regex, '');
// name=value
regex = new RegExp('(' + name + '=)[^\&]+');
url = url.replace(regex, '');
return 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 && self._callBackMappedToRenewStates[expectedState]) {
self._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.config.extraQueryParameter && this.config.extraQueryParameter.indexOf('login_hint') !== -1)) {
this.warn('User login is required');
callback('User login is required', null, 'login required');
return;
}
// renew attempt 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 {
this._requestType = this.REQUEST_TYPE.RENEW_TOKEN;
if (resource === this.config.clientId) {
// App uses idtoken to send to api endpoints
// Default resource is tracked as clientid to store this token
if (this._user) {
this.verbose('renewing idtoken');
this._renewIdToken(callback);
}
else {
this.verbose('renewing idtoken and access_token');
this._renewIdToken(callback, this.RESPONSE_TYPE.ID_TOKEN_TOKEN);
}
} else {
if (this._user) {
this.verbose('renewing access_token');
this._renewToken(resource, callback);
}
else {
this.verbose('renewing idtoken and access_token');
this._renewToken(resource, callback, this.RESPONSE_TYPE.ID_TOKEN_TOKEN);
}
}
}
};
/**
* 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;
this._renewStates.push(expectedState);
this._requestType = this.REQUEST_TYPE.RENEW_TOKEN;
this.verbose('Renew token Expected state: ' + expectedState);
// remove the existing prompt=... query parameter and add prompt=select_account
var urlNavigate = this._urlRemoveQueryStringParameter(this._getNavigateUrl('token', resource), 'prompt');
urlNavigate = urlNavigate + '&prompt=select_account';
if (extraQueryParameters) {
urlNavigate += 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;
}
var callback = this.callback;
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;
this.verbose('Renew token Expected state: ' + expectedState);
// remove the existing prompt=... query parameter and add prompt=select_account
var urlNavigate = this._urlRemoveQueryStringParameter(this._getNavigateUrl('token', resource), 'prompt');
urlNavigate = urlNavigate + '&prompt=select_account';
if (extraQueryParameters) {
urlNavigate += 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, true);
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.infoPii('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.LOGIN_REQUEST, '');
this._saveItem(this.CONSTANTS.STORAGE.ANGULAR_LOGIN_REQUEST, '');
this._saveItem(this.CONSTANTS.STORAGE.SESSION_STATE, '');
this._saveItem(this.CONSTANTS.STORAGE.STATE_LOGIN, '');
this._saveItem(this.CONSTANTS.STORAGE.STATE_RENEW, '');
this._renewStates = [];
this._saveItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN, '');
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, '');
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 && keys[i] !== ""; 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.infoPii('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) {
//If you don�t use prompt=none, then if the session does not exist, there will be a failure.
//If sid is sent alongside domain or login hints, there will be a failure since request is ambiguous.
//If sid is sent with a prompt value other than none or attempt_none, there will be a failure since the request is ambiguous.
if (this._user && this._user.profile) {
if (this._user.profile.sid && urlNavigate.indexOf('&prompt=none') !== -1) {
// don't add sid twice if user provided it in the extraQueryParameter value
if (!this._urlContainsQueryStringParameter("sid", urlNavigate)) {
// add sid
urlNavigate += '&sid=' + encodeURIComponent(this._user.profile.sid);
}
}
else if (this._user.profile.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 (this._matchState(requestInfo)) { // loginRedirect or acquireTokenRedirect
return requestInfo;
}
// external api requests may have many renewtoken requests for different resource
if (!requestInfo.stateMatch && window.parent) {
requestInfo.requestType = this._requestType;
var statesInParentContext = this._renewStates;
for (var i = 0; i < statesInParentContext.length; i++) {
if (statesInParentContext[i] === requestInfo.stateResponse) {
requestInfo.stateMatch = true;
break;
}
}
}
}
}
return requestInfo;
};
/**
* Matches nonce from the request with the response.
* @ignore
*/
AuthenticationContext.prototype._matchNonce = function (user) {
var requestNonce = this._getItem(this.CONSTANTS.STORAGE.NONCE_IDTOKEN);
if (requestNonce) {
requestNonce = requestNonce.split(this.CONSTANTS.CACHE_DELIMETER);
for (var i = 0; i < requestNonce.length; i++) {
if (requestNonce[i] && requestNonce[i] === user.profile.nonce) {
return true;
}
}
}
return false;
};
/**
* Matches state from the request with the response.
* @ignore
*/
AuthenticationContext.prototype._matchState = function (requestInfo) {
var loginStates = this._getItem(this.CONSTANTS.STORAGE.STATE_LOGIN);
if (loginStates) {
loginStates = loginStates.split(this.CONSTANTS.CACHE_DELIMETER);
for (var i = 0; i < loginStates.length; i++) {
if (loginStates[i] && loginStates[i] === requestInfo.stateResponse) {
requestInfo.requestType = this.REQUEST_TYPE.LOGIN;
requestInfo.stateMatch = true;
return true;
}
}
}
var acquireTokenStates = this._getItem(this.CONSTANTS.STORAGE.STATE_RENEW);
if (acquireTokenStates) {
acquireTokenStates = acquireTokenStates.split(this.CONSTANTS.CACHE_DELIMETER);
for (var i = 0; i < acquireTokenStates.length; i++) {
if (acquireTokenStates[i] && acquireTokenStates[i] === requestInfo.stateResponse) {
requestInfo.requestType = this.REQUEST_TYPE.RENEW_TOKEN;
requestInfo.stateMatch = true;
return true;
}
}
}
return false;
};
/**
* 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 err