@kwiz/common
Version:
KWIZ common utilities and helpers for M365 platform
383 lines • 18 kB
JavaScript
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