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

Custom-INetworkModule-and-Network-Tracing sample now supports an axios HttpClient as well as requests running on an express server. #5658

Merged
merged 4 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import {
INetworkModule,
NetworkRequestOptions,
NetworkResponse,
} from "@azure/msal-common";
import axios, { AxiosRequestConfig } from "axios";

enum HttpMethod {
GET = "get",
POST = "post",
}

/**
* This class implements the API for network requests.
*/
export class HttpClientAxios implements INetworkModule {

/**
* Http Get request
* @param url
* @param options
*/
async sendGetRequestAsync<T>(
url: string,
options?: NetworkRequestOptions
): Promise<NetworkResponse<T>> {
const request: AxiosRequestConfig = {
method: HttpMethod.GET,
url: url,
/* istanbul ignore next */
headers: options && options.headers,
/* istanbul ignore next */
validateStatus: () => true
};

const response = await axios(request);
return {
headers: response.headers,
body: response.data as T,
status: response.status,
};
}

/**
* Http Post request
* @param url
* @param options
*/
async sendPostRequestAsync<T>(
url: string,
options?: NetworkRequestOptions,
cancellationToken?: number
): Promise<NetworkResponse<T>> {
const request: AxiosRequestConfig = {
method: HttpMethod.POST,
url: url,
/* istanbul ignore next */
data: (options && options.body) || "",
timeout: cancellationToken,
/* istanbul ignore next */
headers: options && options.headers,
/* istanbul ignore next */
validateStatus: () => true
};

const response = await axios(request);
return {
headers: response.headers,
body: response.data as T,
status: response.status,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,17 @@ class NetworkUtils {
/**
* This class implements the API for network requests.
*/
export class HttpClient implements INetworkModule {
export class HttpClientCurrent implements INetworkModule {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming? Maybe AxiosHttpClient ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe HttpClientNative would be more apt?

private proxyUrl: string;
private customAgentOptions: http.AgentOptions | https.AgentOptions;

constructor(
proxyUrl?: string,
customAgentOptions?: http.AgentOptions | https.AgentOptions,
) {
this.proxyUrl = proxyUrl || "";
this.customAgentOptions = customAgentOptions || {};
}

/**
* Http Get request
Expand All @@ -64,10 +74,10 @@ export class HttpClient implements INetworkModule {
url: string,
options?: NetworkRequestOptions,
): Promise<NetworkResponse<T>> {
if (options?.proxyUrl) {
return networkRequestViaProxy(url, HttpMethod.GET, options);
if (this.proxyUrl) {
return networkRequestViaProxy(url, this.proxyUrl, HttpMethod.GET, options, this.customAgentOptions as http.AgentOptions);
} else {
return networkRequestViaHttps(url, HttpMethod.GET, options);
return networkRequestViaHttps(url, HttpMethod.GET, options, this.customAgentOptions as https.AgentOptions);
}
}

Expand All @@ -81,22 +91,24 @@ export class HttpClient implements INetworkModule {
options?: NetworkRequestOptions,
cancellationToken?: number,
): Promise<NetworkResponse<T>> {
if (options?.proxyUrl) {
return networkRequestViaProxy(url, HttpMethod.POST, options, cancellationToken);
if (this.proxyUrl) {
return networkRequestViaProxy(url, this.proxyUrl, HttpMethod.POST, options, this.customAgentOptions as http.AgentOptions, cancellationToken);
} else {
return networkRequestViaHttps(url, HttpMethod.POST, options, cancellationToken);
return networkRequestViaHttps(url, HttpMethod.POST, options, this.customAgentOptions as https.AgentOptions, cancellationToken);
}
}
}

const networkRequestViaProxy = <T>(
url: string,
proxyUrlString: string,
httpMethod: string,
options: NetworkRequestOptions,
agentOptions?: http.AgentOptions,
timeout?: number,
): Promise<NetworkResponse<T>> => {
const headers = options?.headers || {} as Record<string, string>;
const proxyUrl = new URL(options?.proxyUrl || "");
const proxyUrl = new URL(proxyUrlString);
const destinationUrl = new URL(url);

// "method: connect" must be used to establish a connection to the proxy
Expand All @@ -112,6 +124,10 @@ const networkRequestViaProxy = <T>(
tunnelRequestOptions.timeout = timeout;
}

if (agentOptions && Object.keys(agentOptions).length) {
tunnelRequestOptions.agent = new http.Agent(agentOptions);
}

// compose a request string for the socket
let postRequestStringContent: string = "";
if (httpMethod === HttpMethod.POST) {
Expand Down Expand Up @@ -242,6 +258,7 @@ const networkRequestViaHttps = <T>(
url: string,
httpMethod: string,
options?: NetworkRequestOptions,
agentOptions?: https.AgentOptions,
timeout?: number,
): Promise<NetworkResponse<T>> => {
const isPostRequest = httpMethod === HttpMethod.POST;
Expand All @@ -257,6 +274,10 @@ const networkRequestViaHttps = <T>(
customOptions.timeout = timeout;
}

if (agentOptions && Object.keys(agentOptions).length) {
customOptions.agent = new https.Agent(agentOptions);
}

if (isPostRequest) {
// needed for post request to work
customOptions.headers = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# MSAL Node Standalone Sample: Custom INetworkModule Implementation and Network Tracing Via "Fiddler Everywhere"

This sample demonstrates how to implement a custom [INetworkModule](https://azuread.github.io/microsoft-authentication-library-for-js/ref/interfaces/_azure_msal_common.inetworkmodule.html) that makes it simple for developers to debug network errors. Additionally, instructions are provided on how to use [Fiddler Everywhere](https://www.telerik.com/fiddler/fiddler-everywhere) to perform a network trace of the application.
This sample demonstrates how to implement a custom [INetworkModule](https://azuread.github.io/microsoft-authentication-library-for-js/ref/interfaces/_azure_msal_common.inetworkmodule.html) that makes it simple for developers to debug network errors. There are two ways to run the sample: one is "as-is" in app.ts, the other is via an express server in express.ts. Additionally, instructions are provided on how to use [Fiddler Everywhere](https://www.telerik.com/fiddler/fiddler-everywhere) to perform a network trace of the application.

Fiddler Everywhere is not supported on all operating systems. [Fiddler Classic](https://www.telerik.com/fiddler/fiddler-classic) is a free Windows-only version of Fiddler Everywhere. It's important to note that AAD no longer supports TLS 1.0, which is the default TLS version in Fiddler Classic. The TLS version can be configured via navigating to Tools > Options > HTTPS, then setting TLS to 1.2.

Expand All @@ -9,7 +9,7 @@ This sample is written in TypeScript and was developed with Node version 16.14.0

## Setup

Locate the folder where `package.json` resides in your terminal. Then type:
In a terminal, navigate to the directory where `package.json` resides. Then type:

```console
npm install
Expand All @@ -19,18 +19,18 @@ Locate the folder where `package.json` resides in your terminal. Then type:

1. Navigate to the [Azure portal](https://portal.azure.com) and select the **Azure AD** service.
2. Select the **App Registrations** blade on the left, then select **New registration**.
3. In the **Register an application page** that appears, enter your application's registration information:
3. In the **Register an application page** that appears, enter registration information:
- In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `Confidential Client Application`.
- Under **Supported account types**, select **Accounts in this organizational directory only**.
4. Select **Register** to create the application.
5. In the app's registration screen, find and note the **Application (client) ID** and **Directory (Tenant) ID**. You use these values in your app's configuration file(s) later.
5. In the app's registration screen, find and note the **Application (client) ID** and **Directory (Tenant) ID**. These values will be used in the app's configuration file(s) later.
6. In the app's registration screen, select the **Certificates & secrets** blade in the left.
- In the **Client secrets** section, select **New client secret**.
- Type a key description (for instance `app_secret`),
- Select one of the available key durations (6 months, 12 months or Custom) as per your security posture.
- The generated key value will be displayed when you select the **Add** button. Copy and save the generated value for use in later steps.
- Select one of the available key durations (6 months, 12 months or Custom).
- The generated key value will be displayed when the **Add** button is selected. Copy and save the generated value for use in later steps.

Before running the sample, you will need to replace the values in the configuration object in app.ts:
Before running the sample, the values in the configuration object in app.ts or express.ts will need to be replaced:

```javascript
const config = {
Expand All @@ -43,21 +43,22 @@ const config = {
};
```

## Implement your own custom INetworkModule
## Implement a custom INetworkModule

There are two approaches to implementing a custom INetworkModule in this sample.
1. The default msal-node (as of v1.14.3) INetworkModule has been copied to HttpClient.ts and can be imported to app.ts to be used as custom INetworkModule. You can edit HttpClient.ts to include console.log()'s to see how network traffic is processed.
2. You can implement your own custom INetworkModule inline. Stubs to mock the default implementation of INetworkModule have been provided.
There are three approaches to implementing a custom INetworkModule in this sample.
1. The default msal-node (as of v1.15.0) INetworkModule has been copied to HttpClientCurrent.ts and can be imported to app.ts or express.ts to be used as a custom INetworkModule. HttpClientCurrent.ts can be edited to include console.log()'s to see how network traffic is processed.
2. The pre-proxy-support msal-node INetworkModule has been copied to HttpClientAxios.ts and can be imported to app.ts or express.ts to be used as a custom INetworkModule. HttpClientAxios.ts can be edited to include console.log()'s to see how network traffic is processed. `NOTE: Axios does not support proxy functionality. Therefore, neither does HttpClientAxios.`
3. A custom INetworkModule can be implemeted inline in the system configuration. Stubs to mock the default implementation of INetworkModule have been provided.

## Use Fiddler Everywhere to perform a network trace
Fiddler acts as a proxy and monitors all traffic on your local network
Fiddler acts as a proxy and monitors all traffic on a local network

1. Download and install [Fiddler Everywhere](https://www.telerik.com/download/fiddler-everywhere)
2. Uncomment the proxyUrl line - this tells the sample app to route all traffic through the proxy that Fiddler Everywhere operates on (the port can be configured in the settings of Fiddler Everywhere)

## Run the app

Before running the sample (and everytime you make changes to the sample), the TypeScript will need to be compiled. In the same folder, type:
Before running the sample (and everytime changes are made to the sample), the TypeScript will need to be compiled. In the same folder, type:

```console
npx tsc
Expand All @@ -68,12 +69,30 @@ The sample can now be run by typing:
```console
node dist/app.js
```
or
```console
node dist/express.js
```

An npm script, which will run both of these commands, has been configured in package.json. To compile and start the sample, type:
Two different npm scripts, which will run the above npx and node commands, has been configured in package.json. To compile and start either sample, type:
```console
npm run start
npm run start:app
```
or
```console
npm run start:express
```

### If using `npm run start:app`

The token returned from Azure AD should be immediately displayed in the terminal.

If Fiddler Everywhere was downloaded and installed, HttpClientAxios is not being used as the networkClient, and the proxyUrl line was uncommented: the network requests will be displayed inside of Fiddler Everywhere.

### If using `npm run start:express`

http://localhost:3000 must be navigated to in a browser.

After that, you should see the token returned from Azure AD in your terminal.
The token should then be returned from Azure AD and displayed in the terminal.

If you downloaded, installed, are running Fiddler Everywhere, and uncommented the proxyUrl line, you will see the network requests inside of Fiddler Everywhere.
If Fiddler Everywhere was downloaded and installed, HttpClientAxios is not being used as the networkClient, and the proxyUrl line was uncommented: the network requests will be displayed inside of Fiddler Everywhere.
Original file line number Diff line number Diff line change
@@ -1,41 +1,44 @@
import * as msal from "@azure/msal-node";
// import { INetworkModule, } from "@azure/msal-common";
/**
* After "npx tsc" is executed via the "npm run start" script, app.ts and HttpClient.ts are compiled to .js and stored in the /dist folder
* The app is run via "node dist/app.js", hence the .js import of the HttpClient
* After "npx tsc" is executed via the "npm run start" script, app.ts and HttpClientCurrent.ts are compiled to .js and stored in the /dist folder
* The app is run via "node dist/app.js", hence the .js import of the HttpClientCurrent
*/
// import { HttpClient } from "./HttpClient.js";
// import { HttpClientCurrent } from "./HttpClientCurrent.js";
// import { HttpClientAxios } from "./HttpClientAxios.js";

const clientConfig: msal.Configuration = {
auth: {
clientId: "<ENTER_CLIENT_ID>",
authority: "https://login.microsoftonline.com/<ENTER_TENANT_ID>",
clientSecret: "<ENTER_CLIENT_SECRET>",
},
system: {
system: {
/**
* Uncomment this to see this application's network trace inside of "Fiddler Everywhere" (https://www.telerik.com/download/fiddler-everywhere)
* 8866 is Fiddler Everywhere's default port
* 8888 is Fiddler Classic's default port
* These are both configurable inside of Fiddler's settings
* NOTE: Axios does not support proxy functionality. Therefore, neither does HttpClientAxios. The following 2 lines of code are only supported by HttpClientCurrent.
*/
// proxyUrl: "http://localhost:8866", // Fiddler Everywhere default port
// proxyUrl: "http://localhost:8888", // Fiddler Classic default port

/**
* Uncomment the HttpClient import statement to use this custom INetworkModule
* The contents of ./HttpClient.ts are the default msal-node network functionality, copied from:
* Uncomment either of the HttpClient import statement to use a custom INetworkModule
* The contents of ./HttpClientCurrent.ts are the default msal-node network functionality (msal-node v1.15.0), copied from:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/src/network/HttpClient.ts
* (@azure/msal-node version 1.14.2)
* The contents of ./HttpClientAxios.ts are the msal-node network functionality from when Axios was used - before the HttpClient was rewritten to support proxys - copied from:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/2c77739f9e36538bb68029969d526a3fa37017d7/lib/msal-node/src/network/HttpClient.ts
* Changes were made to account for the imported constants
* console.log()'s can be added to help the developer debug network issues
*/
// networkClient: new HttpClient,

*/
// networkClient: new HttpClientCurrent,
// networkClient: new HttpClientAxios,

/**
* This is the same functionality as the line above. Instead of importing the custom INetworkModule, it can be implemented here
* Uncomment the INetworkModule import to implement this custom INetworkModule
* This is the same functionality as the networkClient lines above. Instead of importing a custom INetworkModule, one can be implemented here.
* Uncomment the INetworkModule import statement to implement the custom INetworkModule below
*/
/** networkClient: new class CustomHttpClient implements INetworkModule {
sendGetRequestAsync<T>(url: string, options?: msal.NetworkRequestOptions, cancellationToken?: number): Promise<msal.NetworkResponse<T>> {
Expand Down
Loading