UNPKG

adal-angular

Version:
1,162 lines (1,007 loc) 82.6 kB
//---------------------------------------------------------------------- // 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