From a7900d6f1a274275e48774c4cd69cffd158d806d Mon Sep 17 00:00:00 2001 From: Igor O Date: Mon, 14 Nov 2022 16:10:44 -0800 Subject: [PATCH 1/4] Switched ARM authentication for editor: - from config params to query params - from CORS proxy to dedicated service --- src/authentication/armAuthenticator.ts | 54 +++++++++++++++++++++----- src/components/app/app.ts | 20 ++++------ src/config.design.json | 13 ++++--- src/constants.ts | 3 ++ src/services/armService.ts | 40 ++++++++++++++++++- 5 files changed, 100 insertions(+), 30 deletions(-) diff --git a/src/authentication/armAuthenticator.ts b/src/authentication/armAuthenticator.ts index cba90de44..e262bf596 100644 --- a/src/authentication/armAuthenticator.ts +++ b/src/authentication/armAuthenticator.ts @@ -1,18 +1,51 @@ import * as Msal from "@azure/msal-browser"; +import { ISettingsProvider } from "@paperbits/common/configuration/ISettingsProvider"; import { IAuthenticator, AccessToken } from "."; +import { SettingNames } from "../constants"; -const aadClientId = "a962e1ed-5694-4abe-9e9b-d08d35877efc"; // test app -const loginRequest = { scopes: ["openid", "profile", "https://management.azure.com/user_impersonation"], account: null }; -const authority = "https://login.microsoftonline.com/common"; -const redirectUri = "https://apimanagement-cors-proxy-df.azure-api.net/portal/signin-aad"; +// const aadClientId = "a962e1ed-5694-4abe-9e9b-d08d35877efc"; // test app PROD +// const aadClientId = "4c6edb5e-d0fb-4ca1-ac29-8c181c1a9522"; // test app PPE + +// const authority = "https://login.microsoftonline.com/common"; // PROD +// const authority = "https://login.windows-ppe.net/common"; // PPE + +// const redirectUri = "https://apimanagement-cors-proxy-df.azure-api.net/portal/signin-aad"; + +// login example +// http://localhost:8080?subscriptionId=b8ff56dc-3bc7-4174-a1e8-3726ab15d0e2&resourceGroupName=Admin-ResourceGroup&serviceName=igo-east export class ArmAuthenticator implements IAuthenticator { private accessToken: AccessToken; + private loginRequest: Msal.SilentRequest; private msalInstance: Msal.PublicClientApplication; private authPromise: Promise; - constructor() { + private initializePromise: Promise; + + constructor( + private readonly settingsProvider: ISettingsProvider + ) {} + + private async ensureInitialized(): Promise { + if (!this.initializePromise) { + this.initializePromise = this.initInstance(); + } + return this.initializePromise; + } + + private async initInstance(): Promise { + const settings = await this.settingsProvider.getSettings(); + const aadClientId = settings[SettingNames.aadClientId]; + const authority = settings[SettingNames.aadAuthority]; + this.loginRequest = settings[SettingNames.aadLoginRequest]; + + if (!aadClientId || !authority || !this.loginRequest) { + throw new Error("Settings was not provided for Msal.Configuration"); + } + + const redirectUri = location.origin; + const msalConfig: Msal.Configuration = { auth: { clientId: aadClientId, @@ -29,8 +62,8 @@ export class ArmAuthenticator implements IAuthenticator { } public async checkCallbacks(): Promise { + await this.ensureInitialized(); try { - return await this.msalInstance.handleRedirectPromise(); } catch (error) { @@ -49,6 +82,7 @@ export class ArmAuthenticator implements IAuthenticator { } private async tryAcquireToken(): Promise { + await this.ensureInitialized(); const account = this.getAccount(); if (!account) { @@ -59,14 +93,14 @@ export class ArmAuthenticator implements IAuthenticator { return parsedToken; } - await this.msalInstance.acquireTokenRedirect(loginRequest); + await this.msalInstance.acquireTokenRedirect(this.loginRequest); return; } - loginRequest.account = account; + this.loginRequest.account = account; try { - const result = await this.msalInstance.acquireTokenSilent(loginRequest); + const result = await this.msalInstance.acquireTokenSilent(this.loginRequest); const token = AccessToken.parse(`${result.tokenType} ${result.accessToken}`); return token; @@ -74,7 +108,7 @@ export class ArmAuthenticator implements IAuthenticator { catch (error) { if (error instanceof Msal.InteractionRequiredAuthError) { // fallback to interaction when silent call fails - await this.msalInstance.acquireTokenRedirect(loginRequest); + await this.msalInstance.acquireTokenRedirect(this.loginRequest); } else { console.warn(error); diff --git a/src/components/app/app.ts b/src/components/app/app.ts index 38b1debc2..47dce21b3 100644 --- a/src/components/app/app.ts +++ b/src/components/app/app.ts @@ -42,24 +42,18 @@ export class App { @OnMounted() public async initialize(): Promise { - const settings = await this.settingsProvider.getSettings(); - - const subscriptionId = settings["subscriptionId"]; - const resourceGroupName = settings[SettingNames.resourceGroupName]; - const serviceName = settings[SettingNames.serviceName]; - const armEndpoint = settings[SettingNames.armEndpoint] || "management.azure.com"; - - if (subscriptionId && resourceGroupName && serviceName) { - const managementApiUrl = `https://${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.ApiManagement/service/${serviceName}`; - await this.settingsProvider.setSetting(SettingNames.managementApiUrl, managementApiUrl); - await this.settingsProvider.setSetting(SettingNames.backendUrl, `https://${serviceName}.developer.azure-api/net`); + try { + await this.armService.loadSessionSettings(); + } catch (error) { + this.viewManager.addToast(startupError, error); + return; } - const runtimeSettings = await this.getRuntimeSettings(); this.sessionManager.setItem("designTimeSettings", runtimeSettings); this.viewManager.setHost({ name: "page-host" }); this.viewManager.showToolboxes(); + const settings = await this.settingsProvider.getSettings(); if (!settings[SettingNames.managementApiUrl]) { this.viewManager.addToast(startupError, `Management API URL is missing. See setting managementApiUrl in the configuration file config.design.json`); @@ -70,7 +64,7 @@ export class App { const developerPortalType = settings[SettingNames.developerPortalType] || DeveloperPortalType.selfHosted; if (developerPortalType === DeveloperPortalType.selfHosted) { this.viewManager.addToast("Warning", WarningBackendUrlMissing); - } + } } try { diff --git a/src/config.design.json b/src/config.design.json index 288ded860..85e1f3e04 100644 --- a/src/config.design.json +++ b/src/config.design.json @@ -1,7 +1,8 @@ -{ - "environment": "development", - "subscriptionId": "< subscription ID >", - "resourceGroupName": "< resource froup name >", - "serviceName": "< service name >", - "useHipCaptcha": false +{ + "environment": "development", + "useHipCaptcha": false, + "armEndpoint": "< armEndpoint >", + "aadClientId": "< aadClientId >", + "aadLoginRequest": "< aadLoginRequest >", + "aadAuthority": "< aadAuthority >" } \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index e67fdab9f..978141376 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -253,6 +253,9 @@ export enum SettingNames { subscriptionId ="subscriptionId", resourceGroupName = "resourceGroupName", serviceName = "serviceName", + aadClientId = "aadClientId", + aadAuthority = "aadAuthority", + aadLoginRequest = "aadLoginRequest", } export enum DeveloperPortalType { diff --git a/src/services/armService.ts b/src/services/armService.ts index 188409bde..ad0a69a8b 100644 --- a/src/services/armService.ts +++ b/src/services/armService.ts @@ -4,6 +4,9 @@ import { HttpClient } from "@paperbits/common/http"; import { IAuthenticator } from "../authentication/IAuthenticator"; import { KnownHttpHeaders } from "../models/knownHttpHeaders"; import { ServiceDescriptionContract } from "../contracts/service"; +import { SettingNames } from "../constants"; + +const sessionLoaded = "ArmSessionLoaded"; export class AzureResourceManagementService { constructor( @@ -18,7 +21,7 @@ export class AzureResourceManagementService { public async getServiceDescription(): Promise { const managementApiUrl = await this.settingsProvider.getSetting(Constants.SettingNames.managementApiUrl); const armAccessToken = await this.authenticator.getAccessTokenAsString(); - + const serviceDescriptionResponse = await this.httpClient.send({ url: `${managementApiUrl}?api-version=${Constants.managementApiVersion}`, headers: [{ @@ -61,4 +64,39 @@ export class AzureResourceManagementService { return userTokenValue; } + + public async loadSessionSettings(): Promise { + const url = new URL(location.href); + const subscriptionId = this.getStoredSetting(url, SettingNames.subscriptionId); + const resourceGroupName = this.getStoredSetting(url, SettingNames.resourceGroupName); + const serviceName = this.getStoredSetting(url, SettingNames.serviceName); + + const settings = await this.settingsProvider.getSettings(); + const armEndpoint = this.getStoredSetting(url, SettingNames.armEndpoint) || settings[SettingNames.armEndpoint]; + + if (!subscriptionId || !resourceGroupName || !serviceName || !armEndpoint) { + throw new Error("Required service parameters (like subscription, resource group, service name) were not provided to start editor"); + } + + if (subscriptionId && resourceGroupName && serviceName) { + const managementApiUrl = `https://${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.ApiManagement/service/${serviceName}`; + await this.settingsProvider.setSetting(SettingNames.managementApiUrl, managementApiUrl); + await this.settingsProvider.setSetting(SettingNames.backendUrl, `https://${serviceName}.developer.azure-api/net`); + if(!sessionStorage.getItem(sessionLoaded)) { + sessionStorage.setItem(sessionLoaded, "true"); + location.href = location.origin + location.pathname; + } + } + } + + private getStoredSetting(url: URL, settingName: string): string { + let settingValue = url.searchParams.get(settingName); + if (settingValue) { + settingValue = decodeURIComponent(settingValue); + sessionStorage.setItem(settingName, settingValue); + } else { + settingValue = sessionStorage.getItem(settingName); + } + return settingValue; + } } \ No newline at end of file From db60f6fdd211b8843d3cd860608b0da7a47aee5d Mon Sep 17 00:00:00 2001 From: Igor O Date: Mon, 14 Nov 2022 16:28:55 -0800 Subject: [PATCH 2/4] small fix --- src/services/armService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/armService.ts b/src/services/armService.ts index ad0a69a8b..f31474de3 100644 --- a/src/services/armService.ts +++ b/src/services/armService.ts @@ -81,7 +81,7 @@ export class AzureResourceManagementService { if (subscriptionId && resourceGroupName && serviceName) { const managementApiUrl = `https://${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.ApiManagement/service/${serviceName}`; await this.settingsProvider.setSetting(SettingNames.managementApiUrl, managementApiUrl); - await this.settingsProvider.setSetting(SettingNames.backendUrl, `https://${serviceName}.developer.azure-api/net`); + await this.settingsProvider.setSetting(SettingNames.backendUrl, `https://${serviceName}.developer.azure-api.net`); if(!sessionStorage.getItem(sessionLoaded)) { sessionStorage.setItem(sessionLoaded, "true"); location.href = location.origin + location.pathname; From 2a3d3c868132c230cf29fe1b98cdd59478115b29 Mon Sep 17 00:00:00 2001 From: Igor O Date: Tue, 15 Nov 2022 13:43:05 -0800 Subject: [PATCH 3/4] small refactoring --- src/services/armService.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/services/armService.ts b/src/services/armService.ts index f31474de3..af09e836a 100644 --- a/src/services/armService.ts +++ b/src/services/armService.ts @@ -6,8 +6,6 @@ import { KnownHttpHeaders } from "../models/knownHttpHeaders"; import { ServiceDescriptionContract } from "../contracts/service"; import { SettingNames } from "../constants"; -const sessionLoaded = "ArmSessionLoaded"; - export class AzureResourceManagementService { constructor( private readonly httpClient: HttpClient, @@ -82,8 +80,7 @@ export class AzureResourceManagementService { const managementApiUrl = `https://${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.ApiManagement/service/${serviceName}`; await this.settingsProvider.setSetting(SettingNames.managementApiUrl, managementApiUrl); await this.settingsProvider.setSetting(SettingNames.backendUrl, `https://${serviceName}.developer.azure-api.net`); - if(!sessionStorage.getItem(sessionLoaded)) { - sessionStorage.setItem(sessionLoaded, "true"); + if(url.searchParams.has(SettingNames.subscriptionId)) { location.href = location.origin + location.pathname; } } From ea8ed32820d96ec02305f80bd0995fcb688d19a4 Mon Sep 17 00:00:00 2001 From: Igor O Date: Tue, 15 Nov 2022 14:05:20 -0800 Subject: [PATCH 4/4] removed case dependency for ARM params --- src/services/armService.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/armService.ts b/src/services/armService.ts index af09e836a..6e3a51da1 100644 --- a/src/services/armService.ts +++ b/src/services/armService.ts @@ -64,7 +64,7 @@ export class AzureResourceManagementService { } public async loadSessionSettings(): Promise { - const url = new URL(location.href); + const url = new URL(location.href.toLowerCase()); const subscriptionId = this.getStoredSetting(url, SettingNames.subscriptionId); const resourceGroupName = this.getStoredSetting(url, SettingNames.resourceGroupName); const serviceName = this.getStoredSetting(url, SettingNames.serviceName); @@ -80,13 +80,14 @@ export class AzureResourceManagementService { const managementApiUrl = `https://${armEndpoint}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.ApiManagement/service/${serviceName}`; await this.settingsProvider.setSetting(SettingNames.managementApiUrl, managementApiUrl); await this.settingsProvider.setSetting(SettingNames.backendUrl, `https://${serviceName}.developer.azure-api.net`); - if(url.searchParams.has(SettingNames.subscriptionId)) { + if(url.searchParams.has(SettingNames.subscriptionId.toLowerCase())) { location.href = location.origin + location.pathname; } } } private getStoredSetting(url: URL, settingName: string): string { + settingName = settingName.toLowerCase(); let settingValue = url.searchParams.get(settingName); if (settingValue) { settingValue = decodeURIComponent(settingValue);