UNPKG

msal-iframe-ok

Version:

Fork to allow silent renewal in iFrame of Microsoft Authentication Library for js

637 lines 23.9 kB
// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import * as tslib_1 from "tslib"; import { Constants, SSOTypes } from "./Constants"; import { ClientAuthError } from "./error/ClientAuthError"; import { Library } from "./Constants"; import { Base64 } from "js-base64"; /** * @hidden */ var Utils = /** @class */ (function () { function Utils() { } //#region General Util /** * Utils function to compare two Account objects - used to check if the same user account is logged in * * @param a1: Account object * @param a2: Account object */ Utils.compareAccounts = function (a1, a2) { if (!a1 || !a2) { return false; } if (a1.homeAccountIdentifier && a2.homeAccountIdentifier) { if (a1.homeAccountIdentifier === a2.homeAccountIdentifier) { return true; } } return false; }; /** * Decimal to Hex * * @param num */ Utils.decimalToHex = function (num) { var hex = num.toString(16); while (hex.length < 2) { hex = "0" + hex; } return hex; }; /** * MSAL JS Library Version */ Utils.getLibraryVersion = function () { return Library.version; }; /** * Creates a new random GUID - used to populate state? * @returns string (GUID) */ Utils.createNewGuid = function () { // RFC4122: The version 4 UUID is meant for generating UUIDs from truly-random or // pseudo-random numbers. // The algorithm is as follows: // Set the two most significant bits (bits 6 and 7) of the // clock_seq_hi_and_reserved to zero and one, respectively. // Set the four most significant bits (bits 12 through 15) of the // time_hi_and_version field to the 4-bit version number from // Section 4.1.3. Version4 // Set all the other bits to randomly (or pseudo-randomly) chosen // values. // UUID = time-low "-" time-mid "-"time-high-and-version "-"clock-seq-reserved and low(2hexOctet)"-" node // time-low = 4hexOctet // time-mid = 2hexOctet // time-high-and-version = 2hexOctet // clock-seq-and-reserved = hexOctet: // clock-seq-low = hexOctet // node = 6hexOctet // Format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx // y could be 1000, 1001, 1010, 1011 since most significant two bits needs to be 10 // y values are 8, 9, A, B var cryptoObj = window.crypto; // for IE 11 if (cryptoObj && cryptoObj.getRandomValues) { var buffer = new Uint8Array(16); cryptoObj.getRandomValues(buffer); //buffer[6] and buffer[7] represents the time_hi_and_version field. We will set the four most significant bits (4 through 7) of buffer[6] to represent decimal number 4 (UUID version number). buffer[6] |= 0x40; //buffer[6] | 01000000 will set the 6 bit to 1. buffer[6] &= 0x4f; //buffer[6] & 01001111 will set the 4, 5, and 7 bit to 0 such that bits 4-7 == 0100 = "4". //buffer[8] represents the clock_seq_hi_and_reserved field. We will set the two most significant bits (6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively. buffer[8] |= 0x80; //buffer[8] | 10000000 will set the 7 bit to 1. buffer[8] &= 0xbf; //buffer[8] & 10111111 will set the 6 bit to 0. return Utils.decimalToHex(buffer[0]) + Utils.decimalToHex(buffer[1]) + Utils.decimalToHex(buffer[2]) + Utils.decimalToHex(buffer[3]) + "-" + Utils.decimalToHex(buffer[4]) + Utils.decimalToHex(buffer[5]) + "-" + Utils.decimalToHex(buffer[6]) + Utils.decimalToHex(buffer[7]) + "-" + Utils.decimalToHex(buffer[8]) + Utils.decimalToHex(buffer[9]) + "-" + Utils.decimalToHex(buffer[10]) + Utils.decimalToHex(buffer[11]) + Utils.decimalToHex(buffer[12]) + Utils.decimalToHex(buffer[13]) + Utils.decimalToHex(buffer[14]) + Utils.decimalToHex(buffer[15]); } else { var guidHolder = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"; var hex = "0123456789abcdef"; var r = 0; var guidResponse = ""; for (var i = 0; i < 36; i++) { if (guidHolder[i] !== "-" && guidHolder[i] !== "4") { // each x and y needs to be random r = Math.random() * 16 | 0; } if (guidHolder[i] === "x") { guidResponse += hex[r]; } else if (guidHolder[i] === "y") { // clock-seq-and-reserved first hex is filtered and remaining hex values are random r &= 0x3; // bit and with 0011 to set pos 2 to zero ?0?? r |= 0x8; // set pos 3 to 1 as 1??? guidResponse += hex[r]; } else { guidResponse += guidHolder[i]; } } return guidResponse; } }; //#endregion //#region Time /** * Returns time in seconds for expiration based on string value passed in. * * @param expires */ Utils.expiresIn = function (expires) { // if AAD did not send "expires_in" property, use default expiration of 3599 seconds, for some reason AAD sends 3599 as "expires_in" value instead of 3600 if (!expires) { expires = "3599"; } return this.now() + parseInt(expires, 10); }; /** * return the current time in Unix time. Date.getTime() returns in milliseconds. */ Utils.now = function () { return Math.round(new Date().getTime() / 1000.0); }; //#endregion //#region String Ops /** * Check if a string is empty * * @param str */ Utils.isEmpty = function (str) { return (typeof str === "undefined" || !str || 0 === str.length); }; //#endregion //#region Token Processing (Extract to TokenProcessing.ts) /** * decode a JWT * * @param jwtToken */ Utils.decodeJwt = function (jwtToken) { if (this.isEmpty(jwtToken)) { return null; } var idTokenPartsRegex = /^([^\.\s]*)\.([^\.\s]+)\.([^\.\s]*)$/; var matches = idTokenPartsRegex.exec(jwtToken); if (!matches || matches.length < 4) { //this._requestContext.logger.warn("The returned id_token is not parseable."); return null; } var crackedToken = { header: matches[1], JWSPayload: matches[2], JWSSig: matches[3] }; return crackedToken; }; /** * Extract IdToken by decoding the RAWIdToken * * @param encodedIdToken */ Utils.extractIdToken = function (encodedIdToken) { // id token will be decoded to get the username var decodedToken = this.decodeJwt(encodedIdToken); if (!decodedToken) { return null; } try { var base64IdToken = decodedToken.JWSPayload; var base64Decoded = this.base64DecodeStringUrlSafe(base64IdToken); if (!base64Decoded) { //this._requestContext.logger.info("The returned id_token could not be base64 url safe decoded."); return null; } // ECMA script has JSON built-in support return JSON.parse(base64Decoded); } catch (err) { //this._requestContext.logger.error("The returned id_token could not be decoded" + err); } return null; }; //#endregion //#region Encode and Decode /** * encoding string to base64 - platform specific check * * @param input */ Utils.base64EncodeStringUrlSafe = function (input) { // html5 should support atob function for decoding return Base64.encode(input); }; /** * decoding base64 token - platform specific check * * @param base64IdToken */ Utils.base64DecodeStringUrlSafe = function (base64IdToken) { // html5 should support atob function for decoding base64IdToken = base64IdToken.replace(/-/g, "+").replace(/_/g, "/"); return decodeURIComponent(encodeURIComponent(Base64.decode(base64IdToken))); // jshint ignore:line }; /** * base64 encode a string * * @param input */ // TODO: Rename to specify type of encoding Utils.encode = function (input) { var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; var output = ""; var chr1, chr2, chr3, enc1, enc2, enc3, enc4; var i = 0; input = this.utf8Encode(input); while (i < input.length) { chr1 = input.charCodeAt(i++); chr2 = input.charCodeAt(i++); chr3 = input.charCodeAt(i++); enc1 = chr1 >> 2; enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); enc4 = chr3 & 63; if (isNaN(chr2)) { enc3 = enc4 = 64; } else if (isNaN(chr3)) { enc4 = 64; } output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4); } return output.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); }; /** * utf8 encode a string * * @param input */ Utils.utf8Encode = function (input) { input = input.replace(/\r\n/g, "\n"); var utftext = ""; for (var n = 0; n < input.length; n++) { var c = input.charCodeAt(n); if (c < 128) { utftext += String.fromCharCode(c); } else if ((c > 127) && (c < 2048)) { utftext += String.fromCharCode((c >> 6) | 192); utftext += String.fromCharCode((c & 63) | 128); } else { utftext += String.fromCharCode((c >> 12) | 224); utftext += String.fromCharCode(((c >> 6) & 63) | 128); utftext += String.fromCharCode((c & 63) | 128); } } return utftext; }; /** * decode a base64 token string * * @param base64IdToken */ // TODO: Rename to specify type of encoding Utils.decode = function (base64IdToken) { var codes = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; base64IdToken = String(base64IdToken).replace(/=+$/, ""); var length = base64IdToken.length; if (length % 4 === 1) { throw ClientAuthError.createTokenEncodingError(base64IdToken); } var h1, h2, h3, h4, bits, c1, c2, c3, decoded = ""; for (var i = 0; i < length; i += 4) { //Every 4 base64 encoded character will be converted to 3 byte string, which is 24 bits // then 6 bits per base64 encoded character h1 = codes.indexOf(base64IdToken.charAt(i)); h2 = codes.indexOf(base64IdToken.charAt(i + 1)); h3 = codes.indexOf(base64IdToken.charAt(i + 2)); h4 = codes.indexOf(base64IdToken.charAt(i + 3)); // For padding, if last two are "=" if (i + 2 === length - 1) { bits = h1 << 18 | h2 << 12 | h3 << 6; c1 = bits >> 16 & 255; c2 = bits >> 8 & 255; decoded += String.fromCharCode(c1, c2); break; } // if last one is "=" else if (i + 1 === length - 1) { bits = h1 << 18 | h2 << 12; c1 = bits >> 16 & 255; decoded += String.fromCharCode(c1); break; } bits = h1 << 18 | h2 << 12 | h3 << 6 | h4; // then convert to 3 byte chars c1 = bits >> 16 & 255; c2 = bits >> 8 & 255; c3 = bits & 255; decoded += String.fromCharCode(c1, c2, c3); } return decoded; }; /** * deserialize a string * * @param query */ Utils.deserialize = function (query) { var match; // Regex for replacing addition symbol with a space var pl = /\+/g; var search = /([^&=]+)=([^&]*)/g; var decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); }; var obj = {}; match = search.exec(query); while (match) { obj[decode(match[1])] = decode(match[2]); match = search.exec(query); } return obj; }; //#endregion //#region Scopes (extract to Scopes.ts) /** * Check if there are dup scopes in a given request * * @param cachedScopes * @param scopes */ // TODO: Rename this, intersecting scopes isn't a great name for duplicate checker Utils.isIntersectingScopes = function (cachedScopes, scopes) { cachedScopes = this.convertToLowerCase(cachedScopes); for (var i = 0; i < scopes.length; i++) { if (cachedScopes.indexOf(scopes[i].toLowerCase()) > -1) { return true; } } return false; }; /** * Check if a given scope is present in the request * * @param cachedScopes * @param scopes */ Utils.containsScope = function (cachedScopes, scopes) { cachedScopes = this.convertToLowerCase(cachedScopes); return scopes.every(function (value) { return cachedScopes.indexOf(value.toString().toLowerCase()) >= 0; }); }; /** * toLower * * @param scopes */ // TODO: Rename this, too generic name for a function that only deals with scopes Utils.convertToLowerCase = function (scopes) { return scopes.map(function (scope) { return scope.toLowerCase(); }); }; /** * remove one element from a scope array * * @param scopes * @param scope */ // TODO: Rename this, too generic name for a function that only deals with scopes Utils.removeElement = function (scopes, scope) { return scopes.filter(function (value) { return value !== scope; }); }; //#endregion //#region URL Processing (Extract to UrlProcessing.ts?) Utils.getDefaultRedirectUri = function () { return window.location.href.split("?")[0].split("#")[0]; }; /** * Given a url like https://a:b/common/d?e=f#g, and a tenantId, returns https://a:b/tenantId/d * @param href The url * @param tenantId The tenant id to replace */ Utils.replaceTenantPath = function (url, tenantId) { url = url.toLowerCase(); var urlObject = this.GetUrlComponents(url); var pathArray = urlObject.PathSegments; if (tenantId && (pathArray.length !== 0 && (pathArray[0] === Constants.common || pathArray[0] === SSOTypes.ORGANIZATIONS))) { pathArray[0] = tenantId; } return this.constructAuthorityUriFromObject(urlObject, pathArray); }; Utils.constructAuthorityUriFromObject = function (urlObject, pathArray) { return this.CanonicalizeUri(urlObject.Protocol + "//" + urlObject.HostNameAndPort + "/" + pathArray.join("/")); }; /** * Parses out the components from a url string. * @returns An object with the various components. Please cache this value insted of calling this multiple times on the same url. */ Utils.GetUrlComponents = function (url) { if (!url) { throw "Url required"; } // https://gist.github.com/curtisz/11139b2cfcaef4a261e0 var regEx = RegExp("^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?"); var match = url.match(regEx); if (!match || match.length < 6) { throw "Valid url required"; } var urlComponents = { Protocol: match[1], HostNameAndPort: match[4], AbsolutePath: match[5] }; var pathSegments = urlComponents.AbsolutePath.split("/"); pathSegments = pathSegments.filter(function (val) { return val && val.length > 0; }); // remove empty elements urlComponents.PathSegments = pathSegments; return urlComponents; }; /** * Given a url or path, append a trailing slash if one doesnt exist * * @param url */ Utils.CanonicalizeUri = function (url) { if (url) { url = url.toLowerCase(); } if (url && !Utils.endsWith(url, "/")) { url += "/"; } return url; }; /** * Checks to see if the url ends with the suffix * Required because we are compiling for es5 instead of es6 * @param url * @param str */ // TODO: Rename this, not clear what it is supposed to do Utils.endsWith = function (url, suffix) { if (!url || !suffix) { return false; } return url.indexOf(suffix, url.length - suffix.length) !== -1; }; /** * Utils function to remove the login_hint and domain_hint from the i/p extraQueryParameters * @param url * @param name */ Utils.urlRemoveQueryStringParameter = function (url, name) { if (this.isEmpty(url)) { return url; } 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; }; //#endregion //#region ExtraQueryParameters Processing (Extract?) /** * Constructs extraQueryParameters to be sent to the server for the AuthenticationParameters set by the developer * in any login() or acquireToken() calls * @param idTokenObject * @param extraQueryParameters * @param sid * @param loginHint */ //TODO: check how this behaves when domain_hint only is sent in extraparameters and idToken has no upn. Utils.constructUnifiedCacheQueryParameter = function (request, idTokenObject) { // preference order: account > sid > login_hint var ssoType; var ssoData; var serverReqParam = {}; // if account info is passed, account.sid > account.login_hint if (request) { if (request.account) { var account = request.account; if (account.sid) { ssoType = SSOTypes.SID; ssoData = account.sid; } else if (account.userName) { ssoType = SSOTypes.LOGIN_HINT; ssoData = account.userName; } } // sid from request else if (request.sid) { ssoType = SSOTypes.SID; ssoData = request.sid; } // loginHint from request else if (request.loginHint) { ssoType = SSOTypes.LOGIN_HINT; ssoData = request.loginHint; } } // adalIdToken retrieved from cache else if (idTokenObject) { if (idTokenObject.hasOwnProperty(Constants.upn)) { ssoType = SSOTypes.ID_TOKEN; ssoData = idTokenObject.upn; } else { ssoType = SSOTypes.ORGANIZATIONS; ssoData = null; } } serverReqParam = this.addSSOParameter(ssoType, ssoData); // add the HomeAccountIdentifier info/ domain_hint if (request && request.account && request.account.homeAccountIdentifier) { serverReqParam = this.addSSOParameter(SSOTypes.HOMEACCOUNT_ID, request.account.homeAccountIdentifier, serverReqParam); } return serverReqParam; }; /** * Add SID to extraQueryParameters * @param sid */ Utils.addSSOParameter = function (ssoType, ssoData, ssoParam) { if (!ssoParam) { ssoParam = {}; } if (!ssoData) { return ssoParam; } switch (ssoType) { case SSOTypes.SID: { ssoParam[SSOTypes.SID] = ssoData; break; } case SSOTypes.ID_TOKEN: { ssoParam[SSOTypes.LOGIN_HINT] = ssoData; ssoParam[SSOTypes.DOMAIN_HINT] = SSOTypes.ORGANIZATIONS; break; } case SSOTypes.LOGIN_HINT: { ssoParam[SSOTypes.LOGIN_HINT] = ssoData; break; } case SSOTypes.ORGANIZATIONS: { ssoParam[SSOTypes.DOMAIN_HINT] = SSOTypes.ORGANIZATIONS; break; } case SSOTypes.CONSUMERS: { ssoParam[SSOTypes.DOMAIN_HINT] = SSOTypes.CONSUMERS; break; } case SSOTypes.HOMEACCOUNT_ID: { var homeAccountId = ssoData.split("."); var uid = Utils.base64DecodeStringUrlSafe(homeAccountId[0]); var utid = Utils.base64DecodeStringUrlSafe(homeAccountId[1]); // TODO: domain_req and login_req are not needed according to eSTS team ssoParam[SSOTypes.LOGIN_REQ] = uid; ssoParam[SSOTypes.DOMAIN_REQ] = utid; if (utid === Constants.consumersUtid) { ssoParam[SSOTypes.DOMAIN_HINT] = SSOTypes.CONSUMERS; } else { ssoParam[SSOTypes.DOMAIN_HINT] = SSOTypes.ORGANIZATIONS; } break; } case SSOTypes.LOGIN_REQ: { ssoParam[SSOTypes.LOGIN_REQ] = ssoData; break; } case SSOTypes.DOMAIN_REQ: { ssoParam[SSOTypes.DOMAIN_REQ] = ssoData; break; } } return ssoParam; }; /** * Utility to generate a QueryParameterString from a Key-Value mapping of extraQueryParameters passed * @param extraQueryParameters */ Utils.generateQueryParametersString = function (queryParameters) { var paramsString = null; if (queryParameters) { Object.keys(queryParameters).forEach(function (key) { if (paramsString == null) { paramsString = key + "=" + encodeURIComponent(queryParameters[key]); } else { paramsString += "&" + key + "=" + encodeURIComponent(queryParameters[key]); } }); } return paramsString; }; /** * Check to see if there are SSO params set in the Request * @param request */ Utils.isSSOParam = function (request) { return request && (request.account || request.sid || request.loginHint); }; //#endregion //#region Response Helpers Utils.setResponseIdToken = function (originalResponse, idToken) { var response = tslib_1.__assign({}, originalResponse); response.idToken = idToken; if (response.idToken.objectId) { response.uniqueId = response.idToken.objectId; } else { response.uniqueId = response.idToken.subject; } response.tenantId = response.idToken.tenantId; return response; }; return Utils; }()); export { Utils }; //# sourceMappingURL=Utils.js.map