Skip to content

Commit 639253a

Browse files
authored
Merge pull request #4447 from AzureAD/msal-node-2600-v2
Msal Node adds support `proxy`
2 parents 64decbc + 47cc372 commit 639253a

18 files changed

+128
-33
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Support proxy in msal-node(#4447)",
4+
"packageName": "@azure/msal-common",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Support proxy in msal-node(#4447)",
4+
"packageName": "@azure/msal-node",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

lib/msal-common/src/authority/Authority.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { CloudInstanceDiscoveryResponse, isCloudInstanceDiscoveryResponse } from
1919
import { CloudDiscoveryMetadata } from "./CloudDiscoveryMetadata";
2020
import { RegionDiscovery } from "./RegionDiscovery";
2121
import { RegionDiscoveryMetadata } from "./RegionDiscoveryMetadata";
22+
import { ImdsOptions } from "./ImdsOptions";
2223
import { AzureCloudOptions } from "../config/ClientConfiguration";
2324

2425
/**
@@ -43,15 +44,18 @@ export class Authority {
4344
private regionDiscovery: RegionDiscovery;
4445
// Region discovery metadata
4546
public regionDiscoveryMetadata: RegionDiscoveryMetadata;
47+
// Proxy url string
48+
private proxyUrl: string;
4649

47-
constructor(authority: string, networkInterface: INetworkModule, cacheManager: ICacheManager, authorityOptions: AuthorityOptions) {
50+
constructor(authority: string, networkInterface: INetworkModule, cacheManager: ICacheManager, authorityOptions: AuthorityOptions, proxyUrl?: string) {
4851
this.canonicalAuthority = authority;
4952
this._canonicalAuthority.validateAsUri();
5053
this.networkInterface = networkInterface;
5154
this.cacheManager = cacheManager;
5255
this.authorityOptions = authorityOptions;
5356
this.regionDiscovery = new RegionDiscovery(networkInterface);
5457
this.regionDiscoveryMetadata = { region_used: undefined, region_source: undefined, region_outcome: undefined };
58+
this.proxyUrl = proxyUrl || Constants.EMPTY_STRING;
5559
}
5660

5761
// See above for AuthorityType
@@ -272,7 +276,7 @@ export class Authority {
272276
if (metadata) {
273277
// If the user prefers to use an azure region replace the global endpoints with regional information.
274278
if (this.authorityOptions.azureRegionConfiguration?.azureRegion) {
275-
const autodetectedRegionName = await this.regionDiscovery.detectRegion(this.authorityOptions.azureRegionConfiguration.environmentRegion, this.regionDiscoveryMetadata);
279+
const autodetectedRegionName = await this.regionDiscovery.detectRegion(this.authorityOptions.azureRegionConfiguration.environmentRegion, this.regionDiscoveryMetadata, this.proxyUrl);
276280

277281
const azureRegion = this.authorityOptions.azureRegionConfiguration.azureRegion === Constants.AZURE_REGION_AUTO_DISCOVER_FLAG
278282
? autodetectedRegionName
@@ -336,8 +340,13 @@ export class Authority {
336340
* Gets OAuth endpoints from the given OpenID configuration endpoint.
337341
*/
338342
private async getEndpointMetadataFromNetwork(): Promise<OpenIdConfigResponse | null> {
343+
const options: ImdsOptions = {};
344+
if (this.proxyUrl) {
345+
options.proxyUrl = this.proxyUrl;
346+
}
347+
339348
try {
340-
const response = await this.networkInterface.sendGetRequestAsync<OpenIdConfigResponse>(this.defaultOpenIdConfigurationEndpoint);
349+
const response = await this.networkInterface.sendGetRequestAsync<OpenIdConfigResponse>(this.defaultOpenIdConfigurationEndpoint, options);
341350
return isOpenIdConfigResponse(response.body) ? response.body : null;
342351
} catch (e) {
343352
return null;
@@ -403,9 +412,14 @@ export class Authority {
403412
*/
404413
private async getCloudDiscoveryMetadataFromNetwork(): Promise<CloudDiscoveryMetadata | null> {
405414
const instanceDiscoveryEndpoint = `${Constants.AAD_INSTANCE_DISCOVERY_ENDPT}${this.canonicalAuthority}oauth2/v2.0/authorize`;
415+
const options: ImdsOptions = {};
416+
if (this.proxyUrl) {
417+
options.proxyUrl = this.proxyUrl;
418+
}
419+
406420
let match = null;
407421
try {
408-
const response = await this.networkInterface.sendGetRequestAsync<CloudInstanceDiscoveryResponse>(instanceDiscoveryEndpoint);
422+
const response = await this.networkInterface.sendGetRequestAsync<CloudInstanceDiscoveryResponse>(instanceDiscoveryEndpoint, options);
409423
const metadata = isCloudInstanceDiscoveryResponse(response.body) ? response.body.metadata : [];
410424
if (metadata.length === 0) {
411425
// If no metadata is returned, authority is untrusted

lib/msal-common/src/authority/AuthorityFactory.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ export class AuthorityFactory {
2323
* @param networkClient
2424
* @param protocolMode
2525
*/
26-
static async createDiscoveredInstance(authorityUri: string, networkClient: INetworkModule, cacheManager: ICacheManager, authorityOptions: AuthorityOptions): Promise<Authority> {
26+
static async createDiscoveredInstance(authorityUri: string, networkClient: INetworkModule, cacheManager: ICacheManager, authorityOptions: AuthorityOptions, proxyUrl?: string): Promise<Authority> {
2727
// Initialize authority and perform discovery endpoint check.
28-
const acquireTokenAuthority: Authority = AuthorityFactory.createInstance(authorityUri, networkClient, cacheManager, authorityOptions);
28+
const acquireTokenAuthority: Authority = AuthorityFactory.createInstance(authorityUri, networkClient, cacheManager, authorityOptions, proxyUrl);
2929

3030
try {
3131
await acquireTokenAuthority.resolveEndpointsAsync();
@@ -45,12 +45,12 @@ export class AuthorityFactory {
4545
* @param networkInterface
4646
* @param protocolMode
4747
*/
48-
static createInstance(authorityUrl: string, networkInterface: INetworkModule, cacheManager: ICacheManager, authorityOptions: AuthorityOptions): Authority {
48+
static createInstance(authorityUrl: string, networkInterface: INetworkModule, cacheManager: ICacheManager, authorityOptions: AuthorityOptions, proxyUrl?: string): Authority {
4949
// Throw error if authority url is empty
5050
if (StringUtils.isEmpty(authorityUrl)) {
5151
throw ClientConfigurationError.createUrlEmptyError();
5252
}
5353

54-
return new Authority(authorityUrl, networkInterface, cacheManager, authorityOptions);
54+
return new Authority(authorityUrl, networkInterface, cacheManager, authorityOptions, proxyUrl);
5555
}
5656
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
export type ImdsOptions = {
7+
headers?: {
8+
Metadata: string,
9+
},
10+
proxyUrl?: string,
11+
};

lib/msal-common/src/authority/RegionDiscovery.ts

+19-9
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ import { NetworkResponse } from "../network/NetworkManager";
88
import { IMDSBadResponse } from "../response/IMDSBadResponse";
99
import { Constants, RegionDiscoverySources, ResponseCodes } from "../utils/Constants";
1010
import { RegionDiscoveryMetadata } from "./RegionDiscoveryMetadata";
11+
import { ImdsOptions } from "./ImdsOptions";
1112

1213
export class RegionDiscovery {
1314
// Network interface to make requests with.
1415
protected networkInterface: INetworkModule;
1516
// Options for the IMDS endpoint request
16-
protected static IMDS_OPTIONS = {headers: {"Metadata": "true"}};
17+
protected static IMDS_OPTIONS: ImdsOptions = {
18+
headers: {
19+
Metadata: "true",
20+
},
21+
};
1722

1823
constructor(networkInterface: INetworkModule) {
1924
this.networkInterface = networkInterface;
@@ -24,28 +29,33 @@ export class RegionDiscovery {
2429
*
2530
* @returns Promise<string | null>
2631
*/
27-
public async detectRegion(environmentRegion: string | undefined, regionDiscoveryMetadata: RegionDiscoveryMetadata): Promise<string | null> {
32+
public async detectRegion(environmentRegion: string | undefined, regionDiscoveryMetadata: RegionDiscoveryMetadata, proxyUrl: string): Promise<string | null> {
2833
// Initialize auto detected region with the region from the envrionment
2934
let autodetectedRegionName = environmentRegion;
3035

3136
// Check if a region was detected from the environment, if not, attempt to get the region from IMDS
3237
if (!autodetectedRegionName) {
38+
const options = RegionDiscovery.IMDS_OPTIONS;
39+
if (proxyUrl) {
40+
options.proxyUrl = proxyUrl;
41+
}
42+
3343
try {
34-
const localIMDSVersionResponse = await this.getRegionFromIMDS(Constants.IMDS_VERSION);
44+
const localIMDSVersionResponse = await this.getRegionFromIMDS(Constants.IMDS_VERSION, options);
3545
if (localIMDSVersionResponse.status === ResponseCodes.httpSuccess) {
3646
autodetectedRegionName = localIMDSVersionResponse.body;
3747
regionDiscoveryMetadata.region_source = RegionDiscoverySources.IMDS;
3848
}
3949

4050
// If the response using the local IMDS version failed, try to fetch the current version of IMDS and retry.
4151
if (localIMDSVersionResponse.status === ResponseCodes.httpBadRequest) {
42-
const currentIMDSVersion = await this.getCurrentVersion();
52+
const currentIMDSVersion = await this.getCurrentVersion(options);
4353
if (!currentIMDSVersion) {
4454
regionDiscoveryMetadata.region_source = RegionDiscoverySources.FAILED_AUTO_DETECTION;
4555
return null;
4656
}
4757

48-
const currentIMDSVersionResponse = await this.getRegionFromIMDS(currentIMDSVersion);
58+
const currentIMDSVersionResponse = await this.getRegionFromIMDS(currentIMDSVersion, options);
4959
if (currentIMDSVersionResponse.status === ResponseCodes.httpSuccess) {
5060
autodetectedRegionName = currentIMDSVersionResponse.body;
5161
regionDiscoveryMetadata.region_source = RegionDiscoverySources.IMDS;
@@ -73,18 +83,18 @@ export class RegionDiscovery {
7383
* @param imdsEndpointUrl
7484
* @returns Promise<NetworkResponse<string>>
7585
*/
76-
private async getRegionFromIMDS(version: string): Promise<NetworkResponse<string>> {
77-
return this.networkInterface.sendGetRequestAsync<string>(`${Constants.IMDS_ENDPOINT}?api-version=${version}&format=text`, RegionDiscovery.IMDS_OPTIONS, Constants.IMDS_TIMEOUT);
86+
private async getRegionFromIMDS(version: string, options: ImdsOptions): Promise<NetworkResponse<string>> {
87+
return this.networkInterface.sendGetRequestAsync<string>(`${Constants.IMDS_ENDPOINT}?api-version=${version}&format=text`, options, Constants.IMDS_TIMEOUT);
7888
}
7989

8090
/**
8191
* Get the most recent version of the IMDS endpoint available
8292
*
8393
* @returns Promise<string | null>
8494
*/
85-
private async getCurrentVersion(): Promise<string | null> {
95+
private async getCurrentVersion(options: ImdsOptions): Promise<string | null> {
8696
try {
87-
const response = await this.networkInterface.sendGetRequestAsync<IMDSBadResponse>(`${Constants.IMDS_ENDPOINT}?format=json`, RegionDiscovery.IMDS_OPTIONS);
97+
const response = await this.networkInterface.sendGetRequestAsync<IMDSBadResponse>(`${Constants.IMDS_ENDPOINT}?format=json`, options);
8898

8999
// When IMDS endpoint is called without the api version query param, bad request response comes back with latest version.
90100
if (response.status === ResponseCodes.httpBadRequest && response.body && response.body["newest-versions"] && response.body["newest-versions"].length > 0) {

lib/msal-common/src/client/BaseClient.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export abstract class BaseClient {
109109
const response = await this.networkManager.sendPostRequest<ServerAuthorizationTokenResponse>(
110110
thumbprint,
111111
tokenEndpoint,
112-
{ body: queryString, headers: headers }
112+
{ body: queryString, headers: headers, proxyUrl: this.config.systemOptions.proxyUrl }
113113
);
114114

115115
if (this.config.serverTelemetryManager && response.status < 500 && response.status !== 429) {
@@ -122,7 +122,7 @@ export abstract class BaseClient {
122122

123123
/**
124124
* Updates the authority object of the client. Endpoint discovery must be completed.
125-
* @param updatedAuthority
125+
* @param updatedAuthority
126126
*/
127127
updateAuthority(updatedAuthority: Authority): void {
128128
if (!updatedAuthority.discoveryComplete()) {

lib/msal-common/src/client/DeviceCodeClient.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ export class DeviceCodeClient extends BaseClient {
106106
deviceCodeEndpoint,
107107
{
108108
body: queryString,
109-
headers: headers
109+
headers: headers,
110+
proxyUrl: this.config.systemOptions.proxyUrl
110111
});
111112

112113
return {

lib/msal-common/src/config/ClientConfiguration.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export type AuthOptions = {
8585
export type SystemOptions = {
8686
tokenRenewalOffsetSeconds?: number;
8787
preventCorsPreflight?: boolean;
88+
proxyUrl?: string;
8889
};
8990

9091
/**
@@ -136,7 +137,8 @@ export type AzureCloudOptions = {
136137

137138
export const DEFAULT_SYSTEM_OPTIONS: Required<SystemOptions> = {
138139
tokenRenewalOffsetSeconds: DEFAULT_TOKEN_RENEWAL_OFFSET_SEC,
139-
preventCorsPreflight: false
140+
preventCorsPreflight: false,
141+
proxyUrl: "",
140142
};
141143

142144
const DEFAULT_LOGGER_IMPLEMENTATION: Required<LoggerOptions> = {

lib/msal-common/src/network/INetworkModule.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { NetworkResponse } from "./NetworkManager";
1212
export type NetworkRequestOptions = {
1313
headers?: Record<string, string>,
1414
body?: string;
15+
proxyUrl?: string;
1516
};
1617

1718
/**

lib/msal-common/src/url/UrlString.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class UrlString {
2020
public get urlString(): string {
2121
return this._urlString;
2222
}
23-
23+
2424
constructor(url: string) {
2525
this._urlString = url;
2626
if (StringUtils.isEmpty(this._urlString)) {
@@ -35,7 +35,7 @@ export class UrlString {
3535

3636
/**
3737
* Ensure urls are lower case and end with a / character.
38-
* @param url
38+
* @param url
3939
*/
4040
static canonicalizeUri(url: string): string {
4141
if (url) {
@@ -82,8 +82,8 @@ export class UrlString {
8282

8383
/**
8484
* Given a url and a query string return the url with provided query string appended
85-
* @param url
86-
* @param queryString
85+
* @param url
86+
* @param queryString
8787
*/
8888
static appendQueryString(url: string, queryString: string): string {
8989
if (StringUtils.isEmpty(queryString)) {
@@ -95,7 +95,7 @@ export class UrlString {
9595

9696
/**
9797
* Returns a url with the hash removed
98-
* @param url
98+
* @param url
9999
*/
100100
static removeHashFromUrl(url: string): string {
101101
return UrlString.canonicalizeUri(url.split("#")[0]);
@@ -173,13 +173,13 @@ export class UrlString {
173173

174174
return baseComponents.Protocol + "//" + baseComponents.HostNameAndPort + relativeUrl;
175175
}
176-
176+
177177
return relativeUrl;
178178
}
179-
179+
180180
/**
181181
* Parses hash string from given string. Returns empty string if no hash symbol is found.
182-
* @param hashString
182+
* @param hashString
183183
*/
184184
static parseHash(hashString: string): string {
185185
const hashIndex1 = hashString.indexOf("#");

lib/msal-node/docs/configuration.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ const msalConfig = {
5454
},
5555
piiLoggingEnabled: false,
5656
logLevel: msal.LogLevel.Verbose,
57-
}
57+
},
58+
proxyUrl: "",
5859
}
5960
}
6061

@@ -83,6 +84,7 @@ const msalInstance = new PublicClientApplication(msalConfig);
8384
| ------ | ----------- | ------ | ------------- |
8485
| `loggerOptions` | Config object for logger. | See [below](#logger-config-options). | See [below](#logger-config-options). |
8586
| `NetworkClient` | Custom HTTP implementation | INetworkModule | Coming Soon |
87+
| `proxyUrl` | The URL of the proxy the app is running behind | string | Empty string `""` |
8688

8789
### Logger Config Options
8890
| Option | Description | Format | Default Value |

lib/msal-node/docs/faq.md

+3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ If you want to work around this, please note:
5252
### How do I implement self-service sign-up with MSAL Node?
5353
MSAL Node supports self-service sign-up in the auth code flow. Please see our docs [here](https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_node.html#authorizationurlrequest) for supported prompt values in the request and their expected outcomes, and [here](http://aka.ms/s3u) for an overview of self-service sign-up and configuration changes that need to be made to your Azure tenant. Please note that that self-service sign-up is not available in B2C and test environments.
5454

55+
### Why doesn't my app function correctly when it's running behind a proxy?
56+
MSAL Node uses Axios as the default network manager. However, Axios does not support proxies. To mitigate this, MSAL Node now supports proxies by utilizing [this workaround](https://github.com/axios/axios/issues/2072#issuecomment-567473812). Developers can provide a `proxyUrl` string in the system config options as detailed [here](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/configuration.md#system-config-options). Developers can also implement their own NetworkManager by instantiating an INetworkModule and building proxy support in it.
57+
5558
## B2C
5659

5760
### How do I handle the password-reset user-flow?

lib/msal-node/package-lock.json

+17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/msal-node/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"dependencies": {
6161
"@azure/msal-common": "^6.0.0",
6262
"axios": "^0.21.4",
63+
"https-proxy-agent": "^5.0.0",
6364
"jsonwebtoken": "^8.5.1",
6465
"uuid": "^8.3.0"
6566
},

lib/msal-node/src/client/ClientApplication.ts

+4
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,9 @@ export abstract class ClientApplication {
317317
authority: discoveredAuthority,
318318
clientCapabilities: this.config.auth.clientCapabilities
319319
},
320+
systemOptions: {
321+
proxyUrl: this.config.system.proxyUrl,
322+
},
320323
loggerOptions: {
321324
logLevel: this.config.system.loggerOptions.logLevel,
322325
loggerCallback: this.config.system.loggerOptions
@@ -407,6 +410,7 @@ export abstract class ClientApplication {
407410
authorityMetadata: this.config.auth.authorityMetadata,
408411
azureRegionConfiguration
409412
};
413+
410414
return await AuthorityFactory.createDiscoveredInstance(authorityUrl, this.config.system.networkClient, this.storage, authorityOptions);
411415
}
412416
}

0 commit comments

Comments
 (0)