UNPKG

@kwiz/common

Version:

KWIZ common utilities and helpers for M365 platform

383 lines 18 kB
import { firstOrNull } from "../../helpers/collections.base"; import { promiseLock } from "../../helpers/promises"; import { getUniqueId } from "../../helpers/random"; import { isSPPageContextInfoReady, isSPPageContextInfoReadySync } from "../../helpers/sharepoint"; import { isNullOrEmptyString, isNullOrUndefined, isNumber, isNumeric, isTypeofFullNameNullOrUndefined } from "../../helpers/typecheckers"; import { makeFullUrl } from "../../helpers/url"; import { SPFxAuthTokenType } from "../../types/auth"; import { ConsoleLogger } from "../consolelogger"; import { getCacheItem, setCacheItem } from "../localstoragecache"; import { GetJson, GetJsonSync } from "../rest"; import { GetRestBaseUrl, hasGlobalContext } from "../sharepoint.rest/common"; import { AutoDiscoverTenantInfo, DiscoverTenantInfo } from "./discovery"; const logger = ConsoleLogger.get("utils/auth/common"); export function GetTokenAudiencePrefix(appId) { return `api://${appId}`; } export function GetDefaultScope(appId) { return `${GetTokenAudiencePrefix(appId)}/access_as_user`; } export function GetMSALSiteScope(hostName) { return `https://${hostName}`; } function _getGraphUrlFromHost(loginHostOrsharePointHost) { loginHostOrsharePointHost = loginHostOrsharePointHost.toLowerCase(); if (loginHostOrsharePointHost.endsWith('/')) { return loginHostOrsharePointHost.slice(0, -1); } if (loginHostOrsharePointHost.endsWith(".us")) { return "https://graph.microsoft.us"; } else if (loginHostOrsharePointHost.endsWith(".sharepoint.com") || loginHostOrsharePointHost.endsWith("login.microsoftonline.com")) { return "https://graph.microsoft.com"; } else if (loginHostOrsharePointHost.endsWith(".de")) { return "https://graph.microsoft.de"; } else if (loginHostOrsharePointHost.endsWith(".cn")) { return "https://microsoftgraph.chinacloudapi.cn"; } return null; } export function GetGraphEndpointUrl() { let url = ""; if ("location" in globalThis && !isNullOrEmptyString(globalThis.location.host)) { url = _getGraphUrlFromHost(globalThis.location.host); } if (isNullOrEmptyString(url) && hasGlobalContext() === true) { if (!isNullOrEmptyString(_spPageContextInfo["msGraphEndpointUrl"])) { url = _spPageContextInfo["msGraphEndpointUrl"]; } if (isNullOrEmptyString(url) && !isNullOrEmptyString(_spPageContextInfo["aadInstanceUrl"])) { url = _getGraphUrlFromHost(_spPageContextInfo["aadInstanceUrl"]); } if (isNullOrEmptyString(url) && !isNullOrEmptyString(_spPageContextInfo["aadTenantId"])) { let tenantInfo = DiscoverTenantInfo(_spPageContextInfo["aadTenantId"], true); if (!isNullOrUndefined(tenantInfo) && !isNullOrEmptyString(tenantInfo.msGraphHost)) { url = `https://${tenantInfo.msGraphHost}`; } } } if (isNullOrEmptyString(url)) { let tenantInfo = AutoDiscoverTenantInfo(true); if (!isNullOrUndefined(tenantInfo) && !isNullOrEmptyString(tenantInfo.msGraphHost)) { url = `https://${tenantInfo.msGraphHost}`; } } return !isNullOrEmptyString(url) ? url : "https://graph.microsoft.com"; } export function GetMSALAdminConsentUrl(params) { const stateParam = isNullOrEmptyString(params.state) ? '' : `&state=${encodeURIComponent(params.state)}`; const url = `https://login.microsoftonline.com/${params.tenantId || "common"}/adminconsent?client_id=${encodeURIComponent(params.clientId)}&redirect_uri=${encodeURIComponent(params.redirectUri)}${stateParam}`; return url; } function _getGetSPFxClientAuthTokenParams(siteUrl, spfxTokenType = SPFxAuthTokenType.Graph) { let acquireURL = `${GetRestBaseUrl(siteUrl)}/SP.OAuth.Token/Acquire`; //todo: add all the resource end points (ie. OneNote, Yammer, Stream) let resource = ""; let isSPOToken = false; switch (spfxTokenType) { case SPFxAuthTokenType.Outlook: resource = "https://outlook.office365.com/search"; break; case SPFxAuthTokenType.SharePoint: case SPFxAuthTokenType.MySite: isSPOToken = true; let absUrl = makeFullUrl(acquireURL); resource = new URL(absUrl).origin; if (spfxTokenType === SPFxAuthTokenType.MySite) { let split = resource.split("."); split[0] += "-my"; resource = split.join("."); } break; default: resource = GetGraphEndpointUrl(); break; } let data = { resource: resource, tokenType: isSPOToken ? "SPO" : undefined }; let params = { url: acquireURL, body: JSON.stringify(data), options: { allowCache: true, localStorageExpiration: { minutes: 1 }, includeDigestInPost: true, headers: { "Accept": "application/json;odata.metadata=minimal", "content-type": "application/json; charset=UTF-8", "odata-version": "4.0", } } }; return params; } function _parseAndCacheGetSPFxClientAuthTokenResult(result, spfxTokenType = SPFxAuthTokenType.Graph) { if (hasGlobalContext() && !isNullOrUndefined(result) && !isNullOrEmptyString(result.access_token)) { let expiration = new Date(); if (isNumeric(result.expires_on)) { expiration = new Date(parseInt(result.expires_on.toString()) * 1000); } else if (isNumber(result.expires_in)) { expiration.setSeconds(expiration.getSeconds() + result.expires_in); } else { expiration.setSeconds(expiration.getSeconds() + 60 * 30); } setCacheItem(`spfx_access_token_${spfxTokenType}`, result.access_token, expiration); return result.access_token; } return null; } function _getSPFxClientAuthTokenFromCache(spfxTokenType = SPFxAuthTokenType.Graph) { if (hasGlobalContext()) { let cachedToken = getCacheItem(`spfx_access_token_${spfxTokenType}`); if (!isNullOrEmptyString(cachedToken)) { return cachedToken; } } return null; } function _getSPFxClientAuthTokenFromMSALCache(resource, spfxTokenType = SPFxAuthTokenType.Graph) { try { let cachedToken; for (let key in localStorage) { if (key.startsWith(`Identity.OAuth.${_spPageContextInfo.systemUserKey}`) && key.indexOf(resource) !== -1) { cachedToken = JSON.parse(localStorage.getItem(key)); break; } } if (!isNullOrUndefined(cachedToken) && !isNullOrEmptyString(cachedToken.value) && isNumber(cachedToken.expiration) && cachedToken.expiration > new Date().getTime()) { return _parseAndCacheGetSPFxClientAuthTokenResult({ access_token: cachedToken.value, // convert milliseconds to seconds expires_on: Math.floor(new Date(cachedToken.expiration).getTime() / 1000), resource: resource, scope: null, token_type: "Bearer" }, spfxTokenType); } } catch { } return null; } /** Acquire an authorization token for a Outlook, Graph, or SharePoint the same way SPFx clients do */ export async function GetSPFxClientAuthToken(siteUrl, spfxTokenType = SPFxAuthTokenType.Graph) { await isSPPageContextInfoReady(); if (isTypeofFullNameNullOrUndefined("_spPageContextInfo") || _spPageContextInfo.isSPO !== true || _spPageContextInfo.isAppWeb === true || _spPageContextInfo.isAnonymousGuestUser === true || _spPageContextInfo["isEmailAuthenticationGuestUser"] === true) { return null; } let cachedToken = _getSPFxClientAuthTokenFromCache(spfxTokenType); if (!isNullOrEmptyString(cachedToken)) { return cachedToken; } let key = `GetSPFxClientAuthToken_${_spPageContextInfo.userLoginName}_${_spPageContextInfo.siteId}_${spfxTokenType}`; return await promiseLock(key, async () => { if (spfxTokenType === SPFxAuthTokenType.Graph) { let graphResource = GetGraphEndpointUrl(); let token = _getSPFxClientAuthTokenFromMSALCache(graphResource, spfxTokenType); if (!isNullOrEmptyString(token)) { return token; } try { let _spComponentLoader = window["_spComponentLoader"]; let manifests = _spComponentLoader.getManifests(); let manifest = firstOrNull(manifests, (manifest) => { return manifest.alias === "@microsoft/sp-http-base"; }); let module = await _spComponentLoader.loadComponentById(manifest.id); let factory = new module.AadTokenProviderFactory(); let provider = await factory.getTokenProvider(); let token = await provider.getToken(graphResource, true); if (!isNullOrEmptyString(token)) { return _parseAndCacheGetSPFxClientAuthTokenResult({ access_token: token, expires_on: null, resource: graphResource, scope: null, token_type: "Bearer" }, spfxTokenType); } } catch (ex) { logger.error(ex); } try { let bufferToString = (buffer) => { let result = Array.from(buffer, (c) => { return String.fromCodePoint(c); }).join(""); return window.btoa(result); }; let ni = new Uint32Array(1); let ci = () => { let b = window.crypto.getRandomValues(ni); return b[0]; }; let generateNonce = () => { let ti = "0123456789abcdef"; const e = Date.now(), t = 1024 * ci() + (1023 & ci()), n = new Uint8Array(16), a = Math.trunc(t / 2 ** 30), i = t & 2 ** 30 - 1, r = ci(); n[0] = e / 2 ** 40, n[1] = e / 2 ** 32, n[2] = e / 2 ** 24, n[3] = e / 65536, n[4] = e / 256, n[5] = e, n[6] = 112 | a >>> 8, n[7] = a, n[8] = 128 | i >>> 24, n[9] = i >>> 16, n[10] = i >>> 8, n[11] = i, n[12] = r >>> 24, n[13] = r >>> 16, n[14] = r >>> 8, n[15] = r; let o = ""; for (let e = 0; e < n.length; e++) o += ti.charAt(n[e] >>> 4), o += ti.charAt(15 & n[e]), 3 !== e && 5 !== e && 7 !== e && 9 !== e || (o += "-"); return o; }; let requestId = getUniqueId(); let stateBuffer = new TextEncoder().encode(JSON.stringify({ id: getUniqueId(), meta: { interactionType: "silent" } })); let state = bufferToString(stateBuffer); let redirectUri = `https://${window.location.host}/_forms/spfxsinglesignon.aspx`; let sid = _spPageContextInfo["aadSessionId"]; let codeVerifierBuffer = window.crypto.getRandomValues(new Uint8Array(32)); let codeVerifier = bufferToString(codeVerifierBuffer).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_"); let codeChallengeBuffer = await window.crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier)); let codeChallenge = bufferToString(new Uint8Array(codeChallengeBuffer)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_"); let nonce = generateNonce(); let url = `${_spPageContextInfo["aadInstanceUrl"]}/${_spPageContextInfo.aadTenantId}/oauth2/v2.0/authorize?`; url += `client_id=08e18876-6177-487e-b8b5-cf950c1e598c`; url += `&scope=${encodeURIComponent(`${graphResource}/.default openid profile offline_access`)}`; url += `&redirect_uri=${encodeURIComponent(redirectUri)}`; url += `&client-request-id=${encodeURIComponent(requestId)}`; url += `&response_mode=fragment`; url += `&response_type=code`; url += `&code_challenge=${codeChallenge}&code_challenge_method=S256&prompt=none`; url += `&sid=${encodeURIComponent(sid)}&nonce=${nonce}`; url += `&state=${encodeURIComponent(state)}`; let getCodeFromIframe = async () => { return new Promise((resolve, reject) => { try { let iframe = document.createElement("iframe"); iframe.style.visibility = "hidden"; iframe.style.position = "absolute"; iframe.style.width = iframe.style.height = "0"; iframe.style.border = "0"; iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms"); iframe.src = url; iframe.onload = () => { window.setTimeout(() => { let params = new URLSearchParams(iframe.contentWindow.location.hash.replace("#", "?")); let pCode = params.get("code"); let pState = params.get("state"); let pSid = params.get("session_state"); if (!isNullOrEmptyString(pCode) && pState === state && pSid === sid) { resolve(pCode); } else { reject(); } document.body.removeChild(iframe); }, 100); }; document.body.appendChild(iframe); } catch { reject(); } }); }; let authCode = await getCodeFromIframe(); if (!isNullOrEmptyString(authCode)) { let url = `${_spPageContextInfo["aadInstanceUrl"]}/${_spPageContextInfo.aadTenantId}/oauth2/v2.0/token?`; url += `client-request-id=${encodeURIComponent(requestId)}`; let fd = new FormData(); fd.append("client_id", "08e18876-6177-487e-b8b5-cf950c1e598c"); fd.append("scope", `https://${graphResource}/.default openid profile offline_access`); fd.append("redirect_uri", redirectUri); fd.append("code", authCode); fd.append("grant_type", "authorization_code"); fd.append("code_verifier", codeVerifier); let response = await fetch(url, { method: "POST", body: fd }); if (response.ok) { let authToken = await response.json(); return _parseAndCacheGetSPFxClientAuthTokenResult(authToken, spfxTokenType); } } } catch (ex) { logger.error(ex); } } else { try { let { url, body, options } = _getGetSPFxClientAuthTokenParams(siteUrl, spfxTokenType); let result = await GetJson(url, body, options); return _parseAndCacheGetSPFxClientAuthTokenResult(result, spfxTokenType); } catch { } } return null; }, 6000); } /** Acquire an authorization token for a Outlook, Graph, or SharePoint the same way SPFx clients do */ export function GetSPFxClientAuthTokenSync(siteUrl, spfxTokenType = SPFxAuthTokenType.Graph) { isSPPageContextInfoReadySync(); if (isTypeofFullNameNullOrUndefined("_spPageContextInfo") || _spPageContextInfo.isSPO !== true || _spPageContextInfo.isAppWeb === true || _spPageContextInfo.isAnonymousGuestUser === true || _spPageContextInfo["isEmailAuthenticationGuestUser"] === true) { return null; } let cachedToken = _getSPFxClientAuthTokenFromCache(spfxTokenType); if (!isNullOrEmptyString(cachedToken)) { return cachedToken; } if (spfxTokenType === SPFxAuthTokenType.Graph) { let resource = GetGraphEndpointUrl(); let token = _getSPFxClientAuthTokenFromMSALCache(resource, spfxTokenType); if (!isNullOrEmptyString(token)) { return token; } // Cache it for next time using the async method GetSPFxClientAuthToken(siteUrl, spfxTokenType); } else { try { let { url, body, options } = _getGetSPFxClientAuthTokenParams(siteUrl, spfxTokenType); let response = GetJsonSync(url, body, options); return _parseAndCacheGetSPFxClientAuthTokenResult(response.result, spfxTokenType); } catch { } } return null; } //# sourceMappingURL=common.js.map