cosmos-explorer/src/Utils/AuthorizationUtils.ts
Vsevolod Kukol c1bc11d27d
Support multi-tenant switching for Data Plane RBAC (#1988)
* Fix API endpoint for CassandraProxy query API

* activate Mongo Proxy and Cassandra Proxy in Prod

* Add CP Prod endpoint

* Run npm format and tests

* Revert code

* fix bug that blocked local mongo proxy and cassandra proxy development

* Add prod endpoint

* fix pr check tests

* Remove prod

* Remove prod endpoint

* Remove dev endpoint

* Support data plane RBAC

* Support data plane RBAC

* Add additional changes for Portal RBAC functionality

* Remove unnecessary code

* Remove unnecessary code

* Add code to fix VCoreMongo/PG bug

* Address feedback

* Add more logs for RBAC feature

* Add more logs for RBAC features

* Add AAD endpoints for all environments

* Add AAD endpoints

* Run npm format

* Support multi-tenant switching for Data Plane RBAC

* Remove tenantID duplicates

---------

Co-authored-by: Senthamil Sindhu <sindhuba@microsoft.com>
Co-authored-by: Asier Isayas <aisayas@microsoft.com>
2024-10-10 07:36:19 -07:00

182 lines
6.7 KiB
TypeScript

import * as msal from "@azure/msal-browser";
import { Action, ActionModifiers } from "Shared/Telemetry/TelemetryConstants";
import { AuthType } from "../AuthType";
import * as Constants from "../Common/Constants";
import * as Logger from "../Common/Logger";
import { configContext } from "../ConfigContext";
import { DatabaseAccount } from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { trace, traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
import { userContext } from "../UserContext";
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
if (userContext.authType === AuthType.EncryptedToken) {
return {
header: Constants.HttpHeaders.guestAccessToken,
token: userContext.accessToken,
};
} else {
return {
header: Constants.HttpHeaders.authorization,
token: userContext.authorizationToken || "",
};
}
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function decryptJWTToken(token: string) {
if (!token) {
Logger.logError("Cannot decrypt token: No JWT token found", "AuthorizationUtils/decryptJWTToken");
throw new Error("No JWT token found");
}
const tokenParts = token.split(".");
if (tokenParts.length < 2) {
Logger.logError(`Invalid JWT token: ${token}`, "AuthorizationUtils/decryptJWTToken");
throw new Error(`Invalid JWT token: ${token}`);
}
let tokenPayloadBase64: string = tokenParts[1];
tokenPayloadBase64 = tokenPayloadBase64.replace(/-/g, "+").replace(/_/g, "/");
const tokenPayload = decodeURIComponent(
atob(tokenPayloadBase64)
.split("")
.map((p) => "%" + ("00" + p.charCodeAt(0).toString(16)).slice(-2))
.join(""),
);
return JSON.parse(tokenPayload);
}
export async function getMsalInstance() {
const msalConfig: msal.Configuration = {
cache: {
cacheLocation: "localStorage",
},
auth: {
authority: `${configContext.AAD_ENDPOINT}organizations`,
clientId: "203f1145-856a-4232-83d4-a43568fba23d",
},
};
if (process.env.NODE_ENV === "development") {
msalConfig.auth.redirectUri = "https://dataexplorer-dev.azurewebsites.net";
}
const msalInstance = new msal.PublicClientApplication(msalConfig);
return msalInstance;
}
export async function acquireMsalTokenForAccount(
account: DatabaseAccount,
silent: boolean = false,
user_hint?: string,
) {
if (userContext.databaseAccount.properties?.documentEndpoint === undefined) {
throw new Error("Database account has no document endpoint defined");
}
const hrefEndpoint = new URL(userContext.databaseAccount.properties.documentEndpoint).href.replace(
/\/+$/,
"/.default",
);
const msalInstance = await getMsalInstance();
const knownAccounts = msalInstance.getAllAccounts();
// If user_hint is provided, we will try to use it to find the account.
// If no account is found, we will use the current active account or first account in the list.
const msalAccount =
knownAccounts?.filter((account) => account.username === user_hint)[0] ??
msalInstance.getActiveAccount() ??
knownAccounts?.[0];
if (!msalAccount) {
// If no account was found, we need to sign in.
// This will eventually throw InteractionRequiredAuthError if silent is true, we won't handle it here.
const loginRequest = {
scopes: [hrefEndpoint],
loginHint: user_hint ?? userContext.userName,
authority: userContext.tenantId ? `${configContext.AAD_ENDPOINT}${userContext.tenantId}` : undefined,
};
try {
if (silent) {
// We can try to use SSO between different apps to avoid showing a popup.
// With a hint provided, this should work in most cases.
// See https://learn.microsoft.com/en-us/entra/identity-platform/msal-js-sso#sso-between-different-apps
try {
const loginResponse = await msalInstance.ssoSilent(loginRequest);
return loginResponse.accessToken;
} catch (silentError) {
trace(Action.SignInAad, ActionModifiers.Mark, {
request: JSON.stringify(loginRequest),
acquireTokenType: silent ? "silent" : "interactive",
errorMessage: JSON.stringify(silentError),
});
}
}
// If silent acquisition failed, we need to show a popup.
// Passing prompt: "none" will still show a popup but not perform a full sign-in.
// This will only work if the user has already signed in and the session is still valid.
// See https://learn.microsoft.com/en-us/entra/identity-platform/msal-js-prompt-behavior#interactive-requests-with-promptnone
// The hint will be used to pre-fill the username field in the popup if silent is false.
const loginResponse = await msalInstance.loginPopup({ prompt: silent ? "none" : "login", ...loginRequest });
return loginResponse.accessToken;
} catch (error) {
traceFailure(Action.SignInAad, {
request: JSON.stringify(loginRequest),
acquireTokenType: silent ? "silent" : "interactive",
errorMessage: JSON.stringify(error),
});
throw error;
}
} else {
msalInstance.setActiveAccount(msalAccount);
}
const tokenRequest = {
account: msalAccount || null,
forceRefresh: true,
scopes: [hrefEndpoint],
loginHint: user_hint ?? userContext.userName,
authority: `${configContext.AAD_ENDPOINT}${userContext.tenantId ?? msalAccount.tenantId}`,
};
return acquireTokenWithMsal(msalInstance, tokenRequest, silent);
}
export async function acquireTokenWithMsal(
msalInstance: msal.IPublicClientApplication,
request: msal.SilentRequest,
silent: boolean = false,
) {
const tokenRequest = {
account: msalInstance.getActiveAccount() || null,
...request,
};
try {
// attempt silent acquisition first
return (await msalInstance.acquireTokenSilent(tokenRequest)).accessToken;
} catch (silentError) {
if (silentError instanceof msal.InteractionRequiredAuthError && silent === false) {
try {
// The error indicates that we need to acquire the token interactively.
// This will display a pop-up to re-establish authorization. If user does not
// have pop-ups enabled in their browser, this will fail.
return (await msalInstance.acquireTokenPopup(tokenRequest)).accessToken;
} catch (interactiveError) {
traceFailure(Action.SignInAad, {
request: JSON.stringify(tokenRequest),
acquireTokenType: "interactive",
errorMessage: JSON.stringify(interactiveError),
});
throw interactiveError;
}
} else {
traceFailure(Action.SignInAad, {
request: JSON.stringify(tokenRequest),
acquireTokenType: "silent",
errorMessage: JSON.stringify(silentError),
});
throw silentError;
}
}
}