'use strict'; /* globals document */ /* globals localStorage */ /* globals sessionStorage */ /* globals window */ var Logging = { logLevelFilter: 1, log: function (message) { console.log(message); } }; /** * Represents the authentication options. * @public * @class Options * @property {object} additionalLoginRequestParameters Any additional parameters for all authorization requests. * @property {(string|string[])} additionalLoginScope Any additional scope, other than 'openid', for all authorization requests. * @property {string} authority The MYOB authority. The valid values are 'login.myob.com', 'pdv.login.myob.com', 'sit.login.myob.com', and 'dev.login.myob.com'. * @property {string} clientId The client identifier for the JavaScript client. * @property {string} correlationId The correlation identifier for all authorization requests. * @property {string} postLogoutRedirectUri The post-logout redirection endpoint URI for the JavaScript client. * @property {string} redirectUri The redirection endpoint URI for the JavaScript client. * @property {string} storageLocation The storage location for the token cache. The valid values are 'localStorage' and 'sessionStorage'. */ /** * Represents information about the end user. * @class User * @property {object} claims The user claims. * @property {string} userId The user identifier. * @property {string} userName The user name. * @property {string} idToken The encrypted ID token from IDAM. */ /** * Represents a callback function that is invoked after a token for a resource is acquired. * @callback tokenCallback * @param {string} token A token for use by the JavaScript client for a resource. * @param {string} error An error if the token wasn't acquired. */ /** * Creates a new instance of AuthenticationContext. * @constructor * @this {AuthenticationContext} * @param {object} options The authentication options. */ var AuthenticationContext = function (options) { /** * Provides constants. */ this.CONSTANTS = { LOG_LEVEL: { ERROR: 0, WARNING: 1, INFORMATION: 2, VERBOSE: 3 }, LOG_LEVEL_STRING_MAP: { 0: 'ERROR', 1: 'WARNING', 2: 'INFORMATION', 3: 'VERBOSE' }, PARAMETERS: { ACCESS_TOKEN: 'access_token', CLIENT_ID: 'client_id', ISSUED_AT: 'issued_at', ERROR_DESCRIPTION: 'error_description', EXPIRES_ON: 'expires_on', ID_TOKEN: 'id_token', NONCE: 'nonce', REDIRECT_URI: 'redirect_uri', RESOURCE: 'resource', RESPONSE_TYPE: 'response_type', SCOPE: 'scope', STATE: 'state', BRANDING_ID: 'branding_id' }, PROMPTS: { CHANGE_PASSWORD: 'change_password', LOGIN: 'login', RE_AUTHENTICATE: 're_authenticate', NEW_ACCOUNT: 'new_account', NONE: 'none' }, SCOPES: { EMAIL: 'email', PHONE: 'phone' }, STORAGE: { EXPIRATION_KEY: 'ldal.expiration.key', ID_TOKEN: 'ldal.id_token', LOGIN_ERROR: 'ldal.login.error', LOGIN_LOCATION: 'ldal.login.location', NONCE: 'ldal.login.nonce', TOKEN_KEY: 'ldal.token.key', TOKEN_KEYS: 'ldal.token.keys', STATES: 'ldal.login.states' }, TOKEN_KEY_DELIMETER: '|', UI_HINTS: { NEW_ORGANIZATION: 'new_organization', NEW_USER: 'new_user' } }; /** * Represents the request types. */ this.REQUEST_TYPE = { LOGIN: 'LOGIN', RENEW_TOKEN: 'RENEW_TOKEN', UNKNOWN: 'UNKNOWN' }; if (AuthenticationContext.prototype._instance) { return AuthenticationContext.prototype._instance; } AuthenticationContext.prototype._instance = this; // Public fields this.authority = options.authority; this.options = {}; // Private fields this._activeRenewals = {}; this._base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; this._isLoggingIn = false; window.callbacks = {}; window.callbackThunks = {}; if (!options.clientId) { throw new Error('The client identifier is required'); } if (!options.correlationId) { options.correlationId = this._guid(); } this.options = this._cloneOptions(options); if (!this.options.redirectUri) { this.options.redirectUri = window.location.href; } if (!this.options.renewTokenRedirectUri) { this.options.renewTokenRedirectUri = this.options.redirectUri; } }; /** * Acquires a token for use by the JavaScript client for a resource. If the token exists in the token cache and it hasn't expired, then it is loaded from the token cache; otherwise, the token is requested via a hidden iframe. * @param {string} resource The client identifier for the resource. * @param {tokenCallback} callback A function that is called after the token has been acquired. */ AuthenticationContext.prototype.acquireToken = function (resource, callback) { if (typeof callback !== 'function') { throw new Error('callback is not function.'); } if (this._isEmptyString(resource)) { this._logWarning('Resource is missing.'); callback(null, 'Resource is missing.'); return; } var token = this.getToken(resource); if (token) { this._logInformation('An ID or access token for \'' + resource + '\' was found in the token cache.'); callback(token, null); return; } if (this._getStorageItem(this.CONSTANTS.STORAGE.LOGIN_ERROR)) { this._logWarning('Acquiring token for \'' + resource + '\' failed: ' + this._getStorageItem(this.CONSTANTS.STORAGE.LOGIN_ERROR) + '.'); callback(null, this._getStorageItem(this.CONSTANTS.STORAGE.LOGIN_ERROR)); return; } var user = this.getUser(); if (!user) { this._logWarning('Login is required'); callback(null, 'Login is required'); return; } var activeRenewal = this._activeRenewals[resource]; if (activeRenewal && this._now() - activeRenewal.timestamp < 10) { this._addCallback(resource, activeRenewal.state, callback); } else { if (resource === this.options.clientId) { this._logVerbose('An ID token for \'' + resource + '\' was not found in the token cache.'); this._renewIdToken(this.getUser().userName, callback); } else { this._logVerbose('An access token for \'' + resource + '\' was not found in the token cache.'); this._renewAccessToken(resource, callback); } } }; /** * Clear all IDAM related information saved in storage */ AuthenticationContext.prototype.clearStorage = function () { this._removeStorageItem(this.CONSTANTS.STORAGE.ID_TOKEN); this._removeStorageItem(this.CONSTANTS.STORAGE.LOGIN_ERROR); this._removeStorageItem(this.CONSTANTS.STORAGE.LOGIN_LOCATION); var tokenKeys = this._getStorageItem(this.CONSTANTS.STORAGE.TOKEN_KEYS); if (!this._isEmptyString(tokenKeys)) { tokenKeys = tokenKeys.split(this.CONSTANTS.TOKEN_KEY_DELIMETER); for (var index = 0; index < tokenKeys.length; index++) { var key = tokenKeys[index]; this._removeStorageItem(this.CONSTANTS.STORAGE.TOKEN_KEY + '.' + key); this._removeStorageItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + '.' + key); } } this._removeStorageItem(this.CONSTANTS.STORAGE.TOKEN_KEYS); }; /** * Gets the token for a resource from the token cache. * @param {string} resource The client identifier for the resource. * @returns {string} The token, if it exists in the token cache and it hasn't expired; otherwise, null. */ AuthenticationContext.prototype.getToken = function (resource) { if (!this._hasToken(resource)) { return null; } var token = this._getStorageItem(this.CONSTANTS.STORAGE.TOKEN_KEY + '.' + resource); var expires = this._getStorageItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + '.' + resource); if (expires && (expires > this._now() + (this.options.expirationWindowInMinutes || 300))) { return token; } else { this._setStorageItem(this.CONSTANTS.STORAGE.TOKEN_KEY + '.' + resource, ''); this._setStorageItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + '.' + resource, 0); return null; } }; /** * Gets information about the end user. * @returns {User} Information about the end user. */ AuthenticationContext.prototype.getUser = function () { return this._createUser(this.getIdToken()); }; /** * Gets id token saved in storage * @returns {string} id token */ AuthenticationContext.prototype.getIdToken = function () { return this._getStorageItem(this.CONSTANTS.STORAGE.ID_TOKEN); }; /** * Gets the last login error. * @returns {string} The last login error. */ AuthenticationContext.prototype.getLoginError = function () { return this._getStorageItem(this.CONSTANTS.STORAGE.LOGIN_ERROR); }; /** * Processes the authorization response for a log-in or renew token flow. */ AuthenticationContext.prototype.handleOAuth2Callback = function () { var hash = window.location.hash; if (this.isOAuth2Callback(hash)) { var responseInfo = this._getAuthorizationResponse(hash); this._saveAuthorizationResponse(responseInfo); if (window.parent && responseInfo.requestType === this.REQUEST_TYPE.RENEW_TOKEN) { window.src = ''; var callback = window.parent.callbackThunks[responseInfo.state], token = responseInfo.parameters[this.CONSTANTS.PARAMETERS.ACCESS_TOKEN] || responseInfo.parameters[this.CONSTANTS.PARAMETERS.ID_TOKEN], error = this._getStorageItem(this.CONSTANTS.STORAGE.LOGIN_ERROR); callback(token, error); } else { window.location.hash = ''; window.location = this._getStorageItem(this.CONSTANTS.STORAGE.LOGIN_LOCATION); } } }; /** * Parse and save id token from redirection URL */ AuthenticationContext.prototype.parseOAuth2ResponseAndSaveToken = function () { var hash = window.location.hash; if (this.isOAuth2Callback(hash)) { var responseInfo = this._getAuthorizationResponse(hash); this._saveAuthorizationResponse(responseInfo); return { user: this.getUser(), loginState: responseInfo.loginState, loginLocation: this._getStorageItem(this.CONSTANTS.STORAGE.LOGIN_LOCATION) }; } }; /** * Gets a value indicating whether the end user is being logged in. * @returns {boolean} True, if the end user is being logged in; otherwise, false. */ AuthenticationContext.prototype.isLoggingIn = function () { return this._isLoggingIn; }; /** * Gets a value indicating whether the current location contains an authorization response. * @param {string} hash The current location. * @returns {boolean} True, if the current location contains an authorization response; otherwise, false. */ AuthenticationContext.prototype.isOAuth2Callback = function (hash) { hash = this._getHash(hash); var responseParameters = this._deserializeResponseParameters(hash); return responseParameters.hasOwnProperty(this.CONSTANTS.PARAMETERS.ID_TOKEN) || responseParameters.hasOwnProperty(this.CONSTANTS.PARAMETERS.ACCESS_TOKEN) || responseParameters.hasOwnProperty(this.CONSTANTS.PARAMETERS.ERROR_DESCRIPTION); }; /** * Logs the end user in by redirecting the user agent to the authorization endpoint. * Saves the ID token to storage. * @param {string} currentLocation The current location for the JavaScript client. * @param {(string|string[])} additionalScope Any additional scope for the authorization request. * @param {object} additionalRequestParameters Any additional parameters for the authorization request. */ AuthenticationContext.prototype.logIn = function (currentLocation, additionalScope, additionalRequestParameters, loginState) { if (this._isLoggingIn) { return; } var responseType = 'id_token'; if (this.options.loginResource) { responseType += ' token'; } this._setStorageItem(this.CONSTANTS.STORAGE.LOGIN_LOCATION, currentLocation ? currentLocation : window.location); var scope = this._getAuthorizationScope(additionalScope); var stateGuid = this._guid(); var state = stateGuid + "_" + this._encodeBase64String(JSON.stringify(loginState || {})); this._addState(state); var nonce = this._guid(); this._addNonce(nonce); this._setStorageItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, ''); var authorizationRequestUrl = this._getAuthorizationRequestUrl( responseType, this.options.loginResource, scope, state, nonce, this.options.brandingId, this.options.redirectUri, additionalRequestParameters); this._isLoggingIn = true; this._navigate(authorizationRequestUrl); }; /** * Logs the end user out by redirecting the user agent to the end session endpoint. * Deletes tokens from storage. * @returns {} */ AuthenticationContext.prototype.logOut = function () { this.clearStorage(); var endSessionRequestUrl = this._getEndSessionRequestUrl(); this._navigate(endSessionRequestUrl); }; AuthenticationContext.prototype._addCallback = function (resource, state, callback) { if (!this._activeRenewals[resource]) { this._activeRenewals[resource] = { state: state, timestamp: this._now() }; } if (!window.callbacks[state]) { window.callbacks[state] = []; } window.callbacks[state].push(callback); var self = this; if (!window.callbackThunks[state]) { window.callbackThunks[state] = function (token, error) { for (var index = 0; index < window.callbacks[state].length; index++) { var callback = window.callbacks[state][index]; callback(token, error); } window.callbackThunks[state] = null; window.callbacks[state] = null; self._activeRenewals[resource] = null; }; } }; AuthenticationContext.prototype._cloneOptions = function (options) { if (options === null || typeof options !== 'object') { return options; } var clone = {}; for (var option in options) { if (options.hasOwnProperty(option)) { clone[option] = options[option]; } } return clone; }; AuthenticationContext.prototype._convertFromBase64String = function (str) { str = str.replace(/-/g, '+').replace(/_/g, '/'); if (window.atob) { return decodeURIComponent(encodeURIComponent(window.atob(str))); } else { return decodeURIComponent(encodeURIComponent(this._decodeBase64String(str))); } }; AuthenticationContext.prototype._createFrame = function (frameId) { if (this._isEmptyString(frameId)) { return; } var frame = document.getElementById(frameId); if (!frame) { if (document.createElement && document.documentElement && (window.navigator.userAgent.indexOf('MSIE 5.0') === -1 || window.opera)) { var newFrame = document.createElement('iframe'); newFrame.setAttribute('id', frameId); newFrame.style.position = 'absolute'; newFrame.style.visibility = 'hidden'; newFrame.style.width = newFrame.style.height = newFrame.style.borderWidth = '0'; frame = document.getElementsByTagName('body')[0].appendChild(newFrame); } else if (document.body && document.body.insertAdjacentHTML) { document.body.insertAdjacentHTML('beforeEnd', ''); } if (window.frames && window.frames[frameId]) { frame = window.frames[frameId]; } } return frame; }; AuthenticationContext.prototype._saveExpirationTime = function(resource, issuedAt, expiration) { var timeDiff = 0; if (!isNaN(parseInt(issuedAt))) { timeDiff = parseInt(issuedAt) - this._now(); } else { this._logWarning('The token issued time is invalid.'); } this._setStorageItem(this.CONSTANTS.STORAGE.EXPIRATION_KEY + '.' + resource, (expiration - timeDiff)); }; AuthenticationContext.prototype._createUser = function (idToken) { var decodedIdToken = this._parseToken(idToken); if (decodedIdToken && decodedIdToken.hasOwnProperty('aud')) { if (decodedIdToken.aud.toLowerCase() === this.options.clientId.toLowerCase()) { return { claims: decodedIdToken, userId: decodedIdToken.sub, userName: decodedIdToken.username, idToken: idToken }; } else { this._logWarning('The audience claim is invalid.'); } } }; AuthenticationContext.prototype._deserializeResponseParameters = function (hash) { var responseParameters = {}; var regex = /([^&=]+)=?([^&]*)/g; var match = regex.exec(hash); while (match) { responseParameters[decodeURIComponent(match[1].replace(/\+/g, ' '))] = decodeURIComponent(match[2].replace(/\+/g, ' ')); match = regex.exec(hash); } return responseParameters; }; AuthenticationContext.prototype._getAuthorizationRequestUrl = function ( responseType, resource, scope, state, nonce, brandingId, redirectUri, additionalRequestParameters) { return this.authority + '/oauth2/authorize?' + this._serializeAuthorizationRequestParameters( responseType, resource, scope, state, nonce, brandingId, redirectUri, additionalRequestParameters); }; AuthenticationContext.prototype._getAuthorizationResponse = function (hash) { hash = this._getHash(hash); var responseParameters = this._deserializeResponseParameters(hash); var responseInfo = { parameters: {}, requestType: this.REQUEST_TYPE.UNKNOWN, state: '', loginState: {}, stateValid: false, valid: false }; if (responseParameters) { responseInfo.parameters = responseParameters; if (responseParameters.hasOwnProperty(this.CONSTANTS.PARAMETERS.ID_TOKEN) || responseParameters.hasOwnProperty(this.CONSTANTS.PARAMETERS.ACCESS_TOKEN) || responseParameters.hasOwnProperty(this.CONSTANTS.PARAMETERS.ERROR_DESCRIPTION)) { responseInfo.valid = true; if (responseParameters.hasOwnProperty(this.CONSTANTS.PARAMETERS.STATE)) { this._logVerbose('State: ' + responseParameters.state); responseInfo.state = responseParameters.state; if (responseParameters.state.indexOf('_') > 0) { responseInfo.loginState = JSON.parse(this._decodeBase64String(responseParameters.state.split('_')[1])); } } else { this._logVerbose('The state parameter is empty.'); } if (this._validateAndRemoveState(responseInfo.state)) { responseInfo.stateValid = true; responseInfo.requestType = responseInfo.state.indexOf('renew') !== -1 ? this.REQUEST_TYPE.RENEW_TOKEN : this.REQUEST_TYPE.LOGIN; } else { this._logWarning('state is invalid'); } } } return responseInfo; }; AuthenticationContext.prototype._validateAndRemoveState = function (state) { return this._validateAndRemove("state", this.CONSTANTS.STORAGE.STATES, state); }; AuthenticationContext.prototype._validateAndRemoveNonce = function (nonce) { return this._validateAndRemove("nonce", this.CONSTANTS.STORAGE.NONCE, nonce); }; AuthenticationContext.prototype._validateAndRemove = function (target, storageKey, value) { var values = JSON.parse(this._getStorageItem(storageKey) || '[]'); var latestOnes = values.filter(function (o) { return o.issueTime > this._now() - 600; }.bind(this)); var toSaveValues = latestOnes.filter(function (o) { return o[target] !== value; }); this._setStorageItem(storageKey, JSON.stringify(toSaveValues)); var isValid = values.some(function (o) { return o[target] === value; }); return isValid; }; AuthenticationContext.prototype._addState = function (state) { var states = JSON.parse(this._getStorageItem(this.CONSTANTS.STORAGE.STATES) || '[]'); states.push({ state: state, issueTime: this._now() }); this._setStorageItem(this.CONSTANTS.STORAGE.STATES, JSON.stringify(states)); }; AuthenticationContext.prototype._addNonce = function (nonce) { var nonces = JSON.parse(this._getStorageItem(this.CONSTANTS.STORAGE.NONCE) || '[]'); nonces.push({ nonce: nonce, issueTime: this._now() }); this._setStorageItem(this.CONSTANTS.STORAGE.NONCE, JSON.stringify(nonces)); }; AuthenticationContext.prototype._getAuthorizationScope = function ( additionalScope) { var scope = [ 'openid' ]; if (this.options.hasOwnProperty('additionalLoginScope')) { if (this.options.additionalLoginScope instanceof Array) { scope = scope.concat(this.options.additionalLoginScope); } else { scope = scope.concat([ this.options.additionalLoginScope ]); } } if (additionalScope) { if (additionalScope instanceof Array) { scope = scope.concat(additionalScope); } else { scope = scope.concat([ additionalScope ]); } } return scope; }; AuthenticationContext.prototype._getBase64Char = function (input, position) { var index = this._base64Chars.indexOf(input.charAt(position)); if (index === -1) { throw new Error('The Base64 string contains an invalid character.'); } return index; }; AuthenticationContext.prototype._getEndSessionRequestUrl = function () { return this.authority + '/oauth2/logout?' + this._serializeEndSessionRequestParameters(); }; 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; }; AuthenticationContext.prototype._getStorageItem = function (key) { var storage = this._getStorage(); return storage.getItem(key); }; /* jshint ignore:start */ AuthenticationContext.prototype._guid = function () { var value = ''; var format = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; var hex = '0123456789abcdef'; var random = 0; for (var index = 0; index < 36; index++) { if (format[index] !== '-' && format[index] !== '4') { random = Math.random() * 16 | 0; } if (format[index] === 'x') { value += hex[random]; } else if (format[index] === 'y') { random &= 0x3; random |= 0x8; value += hex[random]; } else { value += format[index]; } } return value; }; /* jshint ignore:end */ AuthenticationContext.prototype._hasToken = function (key) { var tokenKeys = this._getStorageItem(this.CONSTANTS.STORAGE.TOKEN_KEYS); return tokenKeys && !this._isEmptyString(tokenKeys) && tokenKeys.indexOf(key) > -1; // TODO: Split key and check if it contains key. }; AuthenticationContext.prototype._isEmptyString = function (str) { return typeof str === 'undefined' || !str || str.length === 0; }; AuthenticationContext.prototype._loadFrame = function (frameId, frameUrl) { var self = this; setTimeout(function () { var frame = self._createFrame(frameId); if (frame.src === '' || frame.src === 'about:blank') { frame.src = frameUrl; self._loadFrame(frameId, frameUrl); } }, 500); }; AuthenticationContext.prototype._log = function (level, message, exception) { if (level <= Logging.logLevelFilter) { var formattedMessage = new Date().toUTCString() + ':' + this.options.correlationId + '-' + this.CONSTANTS.LOG_LEVEL_STRING_MAP[level] + ' ' + message; if (exception) { formattedMessage += '\nstack:\n' + exception.stack; } Logging.log(formattedMessage); } }; AuthenticationContext.prototype._logError = function (message, exception) { this._log(this.CONSTANTS.LOG_LEVEL.ERROR, message, exception); }; AuthenticationContext.prototype._logInformation = function (message) { this._log(this.CONSTANTS.LOG_LEVEL.INFORMATION, message, null); }; AuthenticationContext.prototype._logVerbose = function (message) { this._log(this.CONSTANTS.LOG_LEVEL.VERBOSE, message, null); }; AuthenticationContext.prototype._logWarning = function (message) { this._log(this.CONSTANTS.LOG_LEVEL.WARNING, message, null); }; AuthenticationContext.prototype._navigate = function (url) { if (url) { this._logInformation('Navigating to ' + url); window.location.replace(url); } else { this._logInformation('The navigation URL is empty.'); } }; AuthenticationContext.prototype._now = function () { return Math.round(new Date().getTime() / 1000.0); }; AuthenticationContext.prototype._parseToken = function (token) { var tokenParts = this._readToken(token); if (!tokenParts) { return null; } try { var base64EncodedTokenPayload = tokenParts.jwsPayload; var base64DecodedTokenPayload = this._convertFromBase64String(base64EncodedTokenPayload); if (!base64DecodedTokenPayload) { this._logInformation('The received token could not be parsed from a Base64-encoded string.'); return null; } return JSON.parse(base64DecodedTokenPayload); } catch (ex) { this._logError('The received token could not be parsed.'); } return null; }; AuthenticationContext.prototype._readToken = function (token) { if (token === null) { return null; } var tokenPartsRegex = /^([^\.\s]*)\.([^\.\s]+)\.([^\.\s]*)$/; var matches = tokenPartsRegex.exec(token); if (!matches || matches.length < 4) { this._logWarning('The received token is not a valid JSON Web Token (JWT).'); return null; } var tokenParts = { header: matches[1], jwsPayload: matches[2], jwsSignature: matches[3] }; return tokenParts; }; AuthenticationContext.prototype._removeStorageItem = function (key) { var storage = this._getStorage(); storage.removeItem(key); return true; }; AuthenticationContext.prototype._renewToken = function (responseType, resource, username, callback, scope, nonce) { this._setStorageItem(this.CONSTANTS.STORAGE.LOGIN_LOCATION, ''); if (!this._hasToken(resource)) { var tokenKeys = this._getStorageItem(this.CONSTANTS.STORAGE.TOKEN_KEYS) || ''; this._setStorageItem(this.CONSTANTS.STORAGE.TOKEN_KEYS, tokenKeys + (!this._isEmptyString(tokenKeys) ? this.CONSTANTS.TOKEN_KEY_DELIMETER : '') + resource); } var frame = this._createFrame('ldalRenewTokenFrame' + resource); var state = this._guid() + 'renew' + '|' + resource; this._addCallback(resource, state, callback); this._addState(state); this._setStorageItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, ''); // jshint camelcase:false var authorizationRequestUrl = this._getAuthorizationRequestUrl( responseType, resource !== this.options.clientId ? resource : null, scope, state, nonce, this.options.brandingId, this.options.renewTokenRedirectUri, { prompt: this.CONSTANTS.PROMPTS.NONE, login_hint: username }); frame.src = 'about:blank'; this._loadFrame('ldalRenewTokenFrame' + resource, authorizationRequestUrl); }; AuthenticationContext.prototype._renewAccessToken = function (resource, callback) { this._renewToken('token', resource, this.getUser().userName, callback); }; AuthenticationContext.prototype._renewIdToken = function (username, callback) { var scope = this._getAuthorizationScope(); var nonce = this._guid(); this._addNonce(nonce); this._renewToken( 'id_token', this.options.clientId, username, callback, scope, nonce); }; AuthenticationContext.prototype._saveAuthorizationResponse = function (responseInfo) { this._setStorageItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, ''); if (!responseInfo.parameters.hasOwnProperty(this.CONSTANTS.PARAMETERS.ERROR_DESCRIPTION)) { if (responseInfo.stateValid) { // TODO: Save the session state. var resource, tokenKeys; if (responseInfo.parameters.hasOwnProperty(this.CONSTANTS.PARAMETERS.ID_TOKEN)) { var user = this._createUser(responseInfo.parameters[this.CONSTANTS.PARAMETERS.ID_TOKEN]); if (user) { if (this._validateAndRemoveNonce(user.claims.nonce)) { this._setStorageItem(this.CONSTANTS.STORAGE.ID_TOKEN, responseInfo.parameters[this.CONSTANTS.PARAMETERS.ID_TOKEN]); resource = this.options.clientId; if (!this._hasToken(resource)) { tokenKeys = this._getStorageItem(this.CONSTANTS.STORAGE.TOKEN_KEYS) || ''; this._setStorageItem(this.CONSTANTS.STORAGE.TOKEN_KEYS, tokenKeys + (!this._isEmptyString(tokenKeys) ? this.CONSTANTS.TOKEN_KEY_DELIMETER : '') + resource); } this._setStorageItem(this.CONSTANTS.STORAGE.TOKEN_KEY + '.' + resource, responseInfo.parameters[this.CONSTANTS.PARAMETERS.ID_TOKEN]); this._saveExpirationTime(resource, user.claims.iat, user.claims.exp); } else { this._setStorageItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, 'The received nonce is invalid'); } } this._isLoggingIn = false; } if (responseInfo.parameters.hasOwnProperty(this.CONSTANTS.PARAMETERS.ACCESS_TOKEN)) { resource = responseInfo.state.split('|')[1]; if (!this._hasToken(resource)) { tokenKeys = this._getStorageItem(this.CONSTANTS.STORAGE.TOKEN_KEYS) || ''; this._setStorageItem(this.CONSTANTS.STORAGE.TOKEN_KEYS, tokenKeys + (!this._isEmptyString(tokenKeys) ? this.CONSTANTS.TOKEN_KEY_DELIMETER : '') + resource); } this._setStorageItem(this.CONSTANTS.STORAGE.TOKEN_KEY + '.' + resource, responseInfo.parameters[this.CONSTANTS.PARAMETERS.ACCESS_TOKEN]); this._saveExpirationTime(resource, responseInfo.parameters[this.CONSTANTS.PARAMETERS.ISSUED_AT], responseInfo.parameters[this.CONSTANTS.PARAMETERS.EXPIRES_ON]); } } else { this._setStorageItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, 'The received state is invalid'); if (responseInfo.requestType === this.REQUEST_TYPE.LOGIN) { this._isLoggingIn = false; } } } else { // jshint camelcase:false this._setStorageItem(this.CONSTANTS.STORAGE.LOGIN_ERROR, responseInfo.parameters.error_description); this._logInformation('Error: ' + responseInfo.parameters.error + '; Error description: ' + responseInfo.parameters.error_description + '; Error URI: ' + responseInfo.parameters.error_uri); if (responseInfo.requestType === this.REQUEST_TYPE.LOGIN) { this._isLoggingIn = false; } } }; AuthenticationContext.prototype._serializeAdditionalAuthorizationRequestParameters = function (requestParameters, additionalRequestParameters) { for (var additionalRequestParameterName in additionalRequestParameters) { if (additionalRequestParameters.hasOwnProperty(additionalRequestParameterName)) { requestParameters.push(encodeURIComponent(additionalRequestParameterName) + '=' + encodeURIComponent(additionalRequestParameters[additionalRequestParameterName])); } } }; AuthenticationContext.prototype._serializeAuthorizationRequestParameters = function ( responseType, resource, scope, state, nonce, brandingId, redirectUri, additionalRequestParameters) { var requestParameters = []; if (this.options) { requestParameters.push(this.CONSTANTS.PARAMETERS.RESPONSE_TYPE + '=' + encodeURIComponent(responseType)); requestParameters.push(this.CONSTANTS.PARAMETERS.CLIENT_ID + '=' + encodeURIComponent(this.options.clientId)); requestParameters.push(this.CONSTANTS.PARAMETERS.REDIRECT_URI + '=' + encodeURIComponent(redirectUri)); if (resource) { requestParameters.push(this.CONSTANTS.PARAMETERS.RESOURCE + '=' + encodeURIComponent(resource)); } if (scope) { requestParameters.push(this.CONSTANTS.PARAMETERS.SCOPE + '=' + encodeURIComponent(scope.join(' '))); } if (state) { requestParameters.push(this.CONSTANTS.PARAMETERS.STATE + '=' + encodeURIComponent(state)); } if (nonce) { requestParameters.push(this.CONSTANTS.PARAMETERS.NONCE + '=' + encodeURIComponent(nonce)); } if (brandingId) { requestParameters.push(this.CONSTANTS.PARAMETERS.BRANDING_ID + '=' + brandingId); } if (this.options.hasOwnProperty('additionalLoginRequestParameters')) { this._serializeAdditionalAuthorizationRequestParameters(requestParameters, this.options.additionalLoginRequestParameters); } if (additionalRequestParameters) { this._serializeAdditionalAuthorizationRequestParameters(requestParameters, additionalRequestParameters); } if (this.options.correlationId) { requestParameters.push('client-request-id=' + encodeURIComponent(this.options.correlationId)); } requestParameters.push('x-client-sku=js'); requestParameters.push('x-client-ver=' + this._version()); } return requestParameters.join('&'); }; AuthenticationContext.prototype._serializeEndSessionRequestParameters = function () { var requestParameters = []; requestParameters.push('client_id=' + encodeURIComponent(this.options.clientId)); if (this.options.postLogoutRedirectUri) { requestParameters.push('post_logout_redirect_uri=' + encodeURIComponent(this.options.postLogoutRedirectUri)); } return requestParameters.join('&'); }; AuthenticationContext.prototype._getStorage = function () { if (this.options && this.options.storageLocation && this.options.storageLocation === 'localStorage') { if (!this._supportsLocalStorage()) { this._logInformation('Local storage is not supported'); throw "Local storage is not supported"; } return localStorage; } if (!this._supportsSessionStorage()) { this._logInformation('Session storage is not supported.'); throw "Session storage is not supported."; } return sessionStorage; }; AuthenticationContext.prototype._setStorageItem = function (key, value) { var storage = this._getStorage(); storage.setItem(key, value); }; AuthenticationContext.prototype._storageAvailable = function (type) { try { var storage = window[type], x = '__storage_test__'; storage.setItem(x, x); storage.removeItem(x); return true; } catch(e) { return false; } } AuthenticationContext.prototype._supportsLocalStorage = function () { return this._storageAvailable('localStorage'); }; AuthenticationContext.prototype._supportsSessionStorage = function () { return this._storageAvailable('sessionStorage'); }; AuthenticationContext.prototype.isSupportedByBrowser = function () { var option = this.options.storageLocation; if (option) { return this._storageAvailable(option); } else { return this._supportsLocalStorage() || this._supportsSessionStorage(); }; }; AuthenticationContext.prototype._version = function () { return '1.0.0'; }; AuthenticationContext.prototype._encodeBase64String = function (input) { input = String(input); if (/[^\0-\xFF]/.test(input)) { // Note: no need to special-case astral symbols here, as surrogates are // matched, and the input is supposed to only contain ASCII anyway. throw new Error( 'The string to be encoded contains characters outside of the ' + 'Latin1 range.' ); } var padding = input.length % 3; var output = ''; var position = -1; var a, b, c; var buffer; // Make sure any padding is handled outside of the loop. var length = input.length - padding; while (++position < length) { // Read three bytes, i.e. 24 bits. a = input.charCodeAt(position) << 16; b = input.charCodeAt(++position) << 8; c = input.charCodeAt(++position); buffer = a + b + c; // Turn the 24 bits into four chunks of 6 bits each, and append the // matching character for each of them to the output. output += ( this._base64Chars.charAt(buffer >> 18 & 0x3F) + this._base64Chars.charAt(buffer >> 12 & 0x3F) + this._base64Chars.charAt(buffer >> 6 & 0x3F) + this._base64Chars.charAt(buffer & 0x3F) ); } if (padding == 2) { a = input.charCodeAt(position) << 8; b = input.charCodeAt(++position); buffer = a + b; output += ( this._base64Chars.charAt(buffer >> 10) + this._base64Chars.charAt((buffer >> 4) & 0x3F) + this._base64Chars.charAt((buffer << 2) & 0x3F) + '=' ); } else if (padding == 1) { buffer = input.charCodeAt(position); output += ( this._base64Chars.charAt(buffer >> 2) + this._base64Chars.charAt((buffer << 4) & 0x3F) + '==' ); } return output; }; /* jshint ignore:start */ AuthenticationContext.prototype._decodeBase64String = function (input) { var inputLength = input.length; if (inputLength % 4 !== 0) { throw new Error('The Base64 string is an invalid length.'); } var padding = 0; if (input.charAt(inputLength - 1) === '=') { padding = 1; if (input.charAt(inputLength - 2) === '=') { padding = 2; } inputLength -= 4; } var bits, buffer = [], position; for (position = 0; position < inputLength; position += 4) { var bits = (this._getBase64Char(input, position) << 18) | (this._getBase64Char(input, position + 1) << 12) | (this._getBase64Char(input, position + 2) << 6) | this._getBase64Char(input, position + 3); buffer.push(String.fromCharCode(bits >> 16, (bits >> 8) & 255, bits & 255)); } if (padding === 1) { var bits = (this._getBase64Char(input, position) << 18) | (this._getBase64Char(input, position + 1) << 12) | (this._getBase64Char(input, position + 2) << 6); buffer.push(String.fromCharCode(bits >> 16, (bits >> 8) & 255)); } else if (padding === 2) { var bits = (this._getBase64Char(input, position) << 18) | (this._getBase64Char(input, position + 1) << 12); buffer.push(String.fromCharCode(bits >> 16)); } var output = buffer.join(''); return output; }; /* jshint ignore:end */ if (typeof module !== 'undefined' && module.exports) { module.exports.inject = function (options) { return new AuthenticationContext(options); }; }