mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2025-12-29 22:02:01 +00:00
301 lines
12 KiB
TypeScript
301 lines
12 KiB
TypeScript
import "expose-loader?AuthenticationContext!../../../externals/adal";
|
|
|
|
import Q from "q";
|
|
import * as Constants from "../../Common/Constants";
|
|
import * as DataModels from "../../Contracts/DataModels";
|
|
import { AuthType } from "../../AuthType";
|
|
import * as NotificationConsoleUtils from "../../Utils/NotificationConsoleUtils";
|
|
import { ConsoleDataType } from "../../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
|
|
import { DefaultExperienceUtility } from "../../Shared/DefaultExperienceUtility";
|
|
import * as Logger from "../../Common/Logger";
|
|
import { configContext } from "../../ConfigContext";
|
|
import { userContext } from "../../UserContext";
|
|
import { getErrorMessage } from "../../Common/ErrorHandlingUtils";
|
|
|
|
export default class AuthHeadersUtil {
|
|
public static serverId: string = Constants.ServerIds.productionPortal;
|
|
|
|
private static readonly _firstPartyAppId: string = "203f1145-856a-4232-83d4-a43568fba23d";
|
|
private static readonly _aadEndpoint: string = configContext.AAD_ENDPOINT;
|
|
private static readonly _armEndpoint: string = configContext.ARM_ENDPOINT;
|
|
private static readonly _arcadiaEndpoint: string = configContext.ARCADIA_ENDPOINT;
|
|
private static readonly _armAuthArea: string = configContext.ARM_AUTH_AREA;
|
|
private static readonly _graphEndpoint: string = configContext.GRAPH_ENDPOINT;
|
|
private static readonly _graphApiVersion: string = configContext.GRAPH_API_VERSION;
|
|
|
|
private static _authContext: AuthenticationContext = new AuthenticationContext({
|
|
instance: AuthHeadersUtil._aadEndpoint,
|
|
clientId: AuthHeadersUtil._firstPartyAppId,
|
|
postLogoutRedirectUri: window.location.origin,
|
|
endpoints: {
|
|
aad: AuthHeadersUtil._aadEndpoint,
|
|
graph: AuthHeadersUtil._graphEndpoint,
|
|
armAuthArea: AuthHeadersUtil._armAuthArea,
|
|
armEndpoint: AuthHeadersUtil._armEndpoint,
|
|
arcadiaEndpoint: AuthHeadersUtil._arcadiaEndpoint
|
|
},
|
|
tenant: undefined,
|
|
cacheLocation: window.navigator.userAgent.indexOf("Edge") > -1 ? "localStorage" : undefined
|
|
});
|
|
|
|
public static async getAccessInputMetadata(accessInput: string): Promise<DataModels.AccessInputMetadata> {
|
|
const url = `${configContext.BACKEND_ENDPOINT}${Constants.ApiEndpoints.guestRuntimeProxy}/accessinputmetadata`;
|
|
const authType: string = (<any>window).authType;
|
|
const headers: { [headerName: string]: string } = {};
|
|
|
|
if (authType === AuthType.EncryptedToken) {
|
|
headers[Constants.HttpHeaders.guestAccessToken] = accessInput;
|
|
} else {
|
|
headers[Constants.HttpHeaders.connectionString] = accessInput;
|
|
}
|
|
|
|
let responseText: string;
|
|
try {
|
|
const timeout = setTimeout(() => {
|
|
throw Error("Request timed out while fetching access input metadata");
|
|
}, Constants.ClientDefaults.requestTimeoutMs);
|
|
|
|
const response = await fetch(url, {
|
|
headers,
|
|
method: "GET"
|
|
});
|
|
|
|
clearTimeout(timeout);
|
|
|
|
responseText = await response.text();
|
|
if (!response.ok || !responseText) {
|
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to get access input metadata`);
|
|
throw Error("Failed to get access input metadata");
|
|
}
|
|
} catch (error) {
|
|
const errorMessage: string = getErrorMessage(error);
|
|
NotificationConsoleUtils.logConsoleMessage(
|
|
ConsoleDataType.Error,
|
|
`Error while fetching access input metadata: ${errorMessage}`
|
|
);
|
|
throw error;
|
|
}
|
|
|
|
try {
|
|
const metadata: DataModels.AccessInputMetadata = JSON.parse(JSON.parse(responseText));
|
|
return metadata; // TODO: update to a single JSON parse once backend response is stringified exactly once
|
|
} catch (error) {
|
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, "Failed to parse access input metadata");
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
public static async generateEncryptedToken(): Promise<DataModels.GenerateTokenResponse> {
|
|
const url = configContext.BACKEND_ENDPOINT + "/api/tokens/generateToken" + AuthHeadersUtil._generateResourceUrl();
|
|
const explorer = window.dataExplorer;
|
|
const headers: any = { authorization: userContext.authorizationToken };
|
|
headers[Constants.HttpHeaders.getReadOnlyKey] = !explorer.hasWriteAccess();
|
|
headers[Constants.HttpHeaders.contentType] = "application/json";
|
|
|
|
return await AuthHeadersUtil._initiateGenerateTokenRequest(url, "POST", headers);
|
|
}
|
|
|
|
public static async generateUnauthenticatedEncryptedTokenForConnectionString(
|
|
connectionString: string
|
|
): Promise<DataModels.GenerateTokenResponse> {
|
|
if (!connectionString) {
|
|
return Q.reject("None or empty connection string specified");
|
|
}
|
|
|
|
const url = configContext.BACKEND_ENDPOINT + "/api/guest/tokens/generateToken";
|
|
const headers: any = {};
|
|
headers[Constants.HttpHeaders.connectionString] = connectionString;
|
|
headers[Constants.HttpHeaders.contentType] = "application/json";
|
|
|
|
return await AuthHeadersUtil._initiateGenerateTokenRequest(url, "POST", headers);
|
|
}
|
|
|
|
public static isUserSignedIn(): boolean {
|
|
const user = AuthHeadersUtil._authContext.getCachedUser();
|
|
return !!user;
|
|
}
|
|
|
|
public static getCachedUser(): AuthenticationContext.UserInfo {
|
|
if (this.isUserSignedIn()) {
|
|
return AuthHeadersUtil._authContext.getCachedUser();
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
public static signIn() {
|
|
if (!AuthHeadersUtil.isUserSignedIn()) {
|
|
AuthHeadersUtil._authContext.login();
|
|
}
|
|
}
|
|
|
|
public static signOut() {
|
|
AuthHeadersUtil._authContext.logOut();
|
|
}
|
|
|
|
/**
|
|
* Process token from oauth after login or get cached
|
|
*/
|
|
public static processTokenResponse() {
|
|
const isCallback = AuthHeadersUtil._authContext.isCallback(window.location.hash);
|
|
if (isCallback && !AuthHeadersUtil._authContext.getLoginError()) {
|
|
AuthHeadersUtil._authContext.handleWindowCallback();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get auth token to access apis (Graph, ARM)
|
|
*
|
|
* @param authEndpoint Default to ARM endpoint
|
|
* @param tenantId if tenant id provided, tenant id will set at global. Can be reset with 'common'
|
|
*/
|
|
public static async getAccessToken(
|
|
authEndpoint: string = AuthHeadersUtil._armAuthArea,
|
|
tenantId?: string
|
|
): Promise<string> {
|
|
const AuthorizationType: string = (<any>window).authType;
|
|
if (AuthorizationType === AuthType.EncryptedToken) {
|
|
// setting authorization header to an undefined value causes the browser to exclude
|
|
// the header, which is expected here
|
|
throw new Error("auth type is encrypted token, should not get access token");
|
|
}
|
|
|
|
return new Promise<string>(async (resolve, reject) => {
|
|
if (tenantId) {
|
|
// if tenant id passed in, we will use this tenant id for all the rest calls until next tenant id passed in
|
|
AuthHeadersUtil._authContext.config.tenant = tenantId;
|
|
}
|
|
|
|
AuthHeadersUtil._authContext.acquireToken(
|
|
authEndpoint,
|
|
AuthHeadersUtil._authContext.config.tenant,
|
|
(errorResponse: any, token: any) => {
|
|
if (errorResponse && typeof errorResponse === "string") {
|
|
if (errorResponse.indexOf("login is required") >= 0 || errorResponse.indexOf("AADSTS50058") === 0) {
|
|
// Handle error AADSTS50058: A silent sign-in request was sent but no user is signed in.
|
|
// The user's cached token is invalid, hence we let the user login again.
|
|
AuthHeadersUtil._authContext.login();
|
|
return;
|
|
}
|
|
if (
|
|
this._isMultifactorAuthRequired(errorResponse) ||
|
|
errorResponse.indexOf("AADSTS53000") > -1 ||
|
|
errorResponse.indexOf("AADSTS65001") > -1
|
|
) {
|
|
// Handle error AADSTS50079 and AADSTS50076: User needs to use multifactor authentication and acquireToken fails silent. Redirect
|
|
// Handle error AADSTS53000: User needs to use compliant device to access resource when Conditional Access Policy is set up for user.
|
|
AuthHeadersUtil._authContext.acquireTokenRedirect(
|
|
authEndpoint,
|
|
AuthHeadersUtil._authContext.config.tenant
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
if (errorResponse || !token) {
|
|
Logger.logError(errorResponse, "Hosted/Authorization/_getAuthHeader");
|
|
reject(errorResponse);
|
|
return;
|
|
}
|
|
resolve(token);
|
|
}
|
|
);
|
|
});
|
|
}
|
|
|
|
public static async getPhotoFromGraphAPI(): Promise<Blob> {
|
|
const token = await this.getAccessToken(AuthHeadersUtil._graphEndpoint);
|
|
const headers = new Headers();
|
|
headers.append("Authorization", `Bearer ${token}`);
|
|
|
|
try {
|
|
const response: Response = await fetch(
|
|
`${AuthHeadersUtil._graphEndpoint}/me/thumbnailPhoto?api-version=${AuthHeadersUtil._graphApiVersion}`,
|
|
{
|
|
method: "GET",
|
|
headers: headers
|
|
}
|
|
);
|
|
if (!response.ok) {
|
|
throw response;
|
|
}
|
|
return response.blob();
|
|
} catch (err) {
|
|
return new Blob();
|
|
}
|
|
}
|
|
|
|
private static async _getTenant(subId: string): Promise<string | undefined> {
|
|
if (subId) {
|
|
try {
|
|
// Follow https://github.com/MicrosoftDocs/azure-docs/blob/master/articles/azure-resource-manager/resource-manager-api-authentication.md
|
|
// TenantId will be returned in the header of the response.
|
|
const response: Response = await fetch(
|
|
`https://management.core.windows.net/subscriptions/${subId}?api-version=2015-01-01`
|
|
);
|
|
if (!response.ok) {
|
|
throw response;
|
|
}
|
|
} catch (reason) {
|
|
if (reason.status === 401) {
|
|
const authUrl: string = reason.headers
|
|
.get("www-authenticate")
|
|
.split(",")[0]
|
|
.split("=")[1];
|
|
// Fetch the tenant GUID ID and the length should be 36.
|
|
const tenantId: string = authUrl.substring(authUrl.lastIndexOf("/") + 1, authUrl.lastIndexOf("/") + 37);
|
|
return Promise.resolve(tenantId);
|
|
}
|
|
}
|
|
}
|
|
return Promise.resolve(undefined);
|
|
}
|
|
|
|
private static _isMultifactorAuthRequired(errorResponse: string): boolean {
|
|
for (const code of ["AADSTS50079", "AADSTS50076"]) {
|
|
if (errorResponse.indexOf(code) === 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static _generateResourceUrl(): string {
|
|
const databaseAccount = userContext.databaseAccount;
|
|
const subscriptionId: string = userContext.subscriptionId;
|
|
const resourceGroup = userContext.resourceGroup;
|
|
const defaultExperience: string = DefaultExperienceUtility.getDefaultExperienceFromDatabaseAccount(databaseAccount);
|
|
const apiKind: DataModels.ApiKind = DefaultExperienceUtility.getApiKindFromDefaultExperience(defaultExperience);
|
|
const accountEndpoint = (databaseAccount && databaseAccount.properties.documentEndpoint) || "";
|
|
const sid = subscriptionId || "";
|
|
const rg = resourceGroup || "";
|
|
const dba = (databaseAccount && databaseAccount.name) || "";
|
|
const resourceUrl = encodeURIComponent(accountEndpoint);
|
|
const rid = "";
|
|
const rtype = "";
|
|
return `?resourceUrl=${resourceUrl}&rid=${rid}&rtype=${rtype}&sid=${sid}&rg=${rg}&dba=${dba}&api=${apiKind}`;
|
|
}
|
|
|
|
private static async _initiateGenerateTokenRequest(
|
|
url: string,
|
|
method: string,
|
|
headers: any
|
|
): Promise<DataModels.GenerateTokenResponse> {
|
|
const timeout = setTimeout(() => {
|
|
throw Error("Request timed out while generating token");
|
|
}, Constants.ClientDefaults.requestTimeoutMs);
|
|
|
|
const response = await fetch(url, {
|
|
headers,
|
|
method
|
|
});
|
|
|
|
clearTimeout(timeout);
|
|
|
|
const token: string = await response.json();
|
|
if (response.ok && token) {
|
|
return JSON.parse(token);
|
|
}
|
|
|
|
throw Error("No token generated");
|
|
}
|
|
}
|