Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switched ARM authentication for editor #2001

Open
wants to merge 4 commits into
base: az/arm
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 44 additions & 10 deletions src/authentication/armAuthenticator.ts
Original file line number Diff line number Diff line change
@@ -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<AccessToken>;

constructor() {
private initializePromise: Promise<void>;

constructor(
private readonly settingsProvider: ISettingsProvider
) {}

private async ensureInitialized(): Promise<void> {
if (!this.initializePromise) {
this.initializePromise = this.initInstance();
}
return this.initializePromise;
}

private async initInstance(): Promise<void> {
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,
Expand All @@ -29,8 +62,8 @@ export class ArmAuthenticator implements IAuthenticator {
}

public async checkCallbacks(): Promise<Msal.AuthenticationResult> {
await this.ensureInitialized();
try {

return await this.msalInstance.handleRedirectPromise();
}
catch (error) {
Expand All @@ -49,6 +82,7 @@ export class ArmAuthenticator implements IAuthenticator {
}

private async tryAcquireToken(): Promise<AccessToken> {
await this.ensureInitialized();
const account = this.getAccount();

if (!account) {
Expand All @@ -59,22 +93,22 @@ 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;
}
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);
Expand Down
20 changes: 7 additions & 13 deletions src/components/app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,18 @@ export class App {

@OnMounted()
public async initialize(): Promise<void> {
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 <i>managementApiUrl</i> in the configuration file <i>config.design.json</i>`);
Expand All @@ -70,7 +64,7 @@ export class App {
const developerPortalType = settings[SettingNames.developerPortalType] || DeveloperPortalType.selfHosted;
if (developerPortalType === DeveloperPortalType.selfHosted) {
this.viewManager.addToast("Warning", WarningBackendUrlMissing);
}
}
}

try {
Expand Down
13 changes: 7 additions & 6 deletions src/config.design.json
Original file line number Diff line number Diff line change
@@ -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 >"
}
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ export enum SettingNames {
subscriptionId ="subscriptionId",
resourceGroupName = "resourceGroupName",
serviceName = "serviceName",
aadClientId = "aadClientId",
aadAuthority = "aadAuthority",
aadLoginRequest = "aadLoginRequest",
}

export enum DeveloperPortalType {
Expand Down
38 changes: 37 additions & 1 deletion src/services/armService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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";

export class AzureResourceManagementService {
constructor(
Expand All @@ -18,7 +19,7 @@ export class AzureResourceManagementService {
public async getServiceDescription(): Promise<ServiceDescriptionContract> {
const managementApiUrl = await this.settingsProvider.getSetting<string>(Constants.SettingNames.managementApiUrl);
const armAccessToken = await this.authenticator.getAccessTokenAsString();

const serviceDescriptionResponse = await this.httpClient.send<ServiceDescriptionContract>({
url: `${managementApiUrl}?api-version=${Constants.managementApiVersion}`,
headers: [{
Expand Down Expand Up @@ -61,4 +62,39 @@ export class AzureResourceManagementService {

return userTokenValue;
}

public async loadSessionSettings(): Promise<void> {
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);

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(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);
sessionStorage.setItem(settingName, settingValue);
} else {
settingValue = sessionStorage.getItem(settingName);
}
return settingValue;
}
}