UNPKG

adal-angular

Version:
1,116 lines (978 loc) 73.9 kB
<!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.&lt;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 &lt; 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 &lt; 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 &lt; 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 &lt; 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 &lt; 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