Skip to content

Commit

Permalink
Merge pull request #4447 from AzureAD/msal-node-2600-v2
Browse files Browse the repository at this point in the history
Msal Node adds support `proxy`
  • Loading branch information
sameerag committed Feb 8, 2022
2 parents 64decbc + 47cc372 commit 639253a
Show file tree
Hide file tree
Showing 18 changed files with 128 additions and 33 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Support proxy in msal-node(#4447)",
"packageName": "@azure/msal-common",
"email": "sameera.gajjarapu@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Support proxy in msal-node(#4447)",
"packageName": "@azure/msal-node",
"email": "sameera.gajjarapu@microsoft.com",
"dependentChangeType": "patch"
}
22 changes: 18 additions & 4 deletions lib/msal-common/src/authority/Authority.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { CloudInstanceDiscoveryResponse, isCloudInstanceDiscoveryResponse } from
import { CloudDiscoveryMetadata } from "./CloudDiscoveryMetadata";
import { RegionDiscovery } from "./RegionDiscovery";
import { RegionDiscoveryMetadata } from "./RegionDiscoveryMetadata";
import { ImdsOptions } from "./ImdsOptions";
import { AzureCloudOptions } from "../config/ClientConfiguration";

/**
Expand All @@ -43,15 +44,18 @@ export class Authority {
private regionDiscovery: RegionDiscovery;
// Region discovery metadata
public regionDiscoveryMetadata: RegionDiscoveryMetadata;
// Proxy url string
private proxyUrl: string;

constructor(authority: string, networkInterface: INetworkModule, cacheManager: ICacheManager, authorityOptions: AuthorityOptions) {
constructor(authority: string, networkInterface: INetworkModule, cacheManager: ICacheManager, authorityOptions: AuthorityOptions, proxyUrl?: string) {
this.canonicalAuthority = authority;
this._canonicalAuthority.validateAsUri();
this.networkInterface = networkInterface;
this.cacheManager = cacheManager;
this.authorityOptions = authorityOptions;
this.regionDiscovery = new RegionDiscovery(networkInterface);
this.regionDiscoveryMetadata = { region_used: undefined, region_source: undefined, region_outcome: undefined };
this.proxyUrl = proxyUrl || Constants.EMPTY_STRING;
}

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

const azureRegion = this.authorityOptions.azureRegionConfiguration.azureRegion === Constants.AZURE_REGION_AUTO_DISCOVER_FLAG
? autodetectedRegionName
Expand Down Expand Up @@ -336,8 +340,13 @@ export class Authority {
* Gets OAuth endpoints from the given OpenID configuration endpoint.
*/
private async getEndpointMetadataFromNetwork(): Promise<OpenIdConfigResponse | null> {
const options: ImdsOptions = {};
if (this.proxyUrl) {
options.proxyUrl = this.proxyUrl;
}

try {
const response = await this.networkInterface.sendGetRequestAsync<OpenIdConfigResponse>(this.defaultOpenIdConfigurationEndpoint);
const response = await this.networkInterface.sendGetRequestAsync<OpenIdConfigResponse>(this.defaultOpenIdConfigurationEndpoint, options);
return isOpenIdConfigResponse(response.body) ? response.body : null;
} catch (e) {
return null;
Expand Down Expand Up @@ -403,9 +412,14 @@ export class Authority {
*/
private async getCloudDiscoveryMetadataFromNetwork(): Promise<CloudDiscoveryMetadata | null> {
const instanceDiscoveryEndpoint = `${Constants.AAD_INSTANCE_DISCOVERY_ENDPT}${this.canonicalAuthority}oauth2/v2.0/authorize`;
const options: ImdsOptions = {};
if (this.proxyUrl) {
options.proxyUrl = this.proxyUrl;
}

let match = null;
try {
const response = await this.networkInterface.sendGetRequestAsync<CloudInstanceDiscoveryResponse>(instanceDiscoveryEndpoint);
const response = await this.networkInterface.sendGetRequestAsync<CloudInstanceDiscoveryResponse>(instanceDiscoveryEndpoint, options);
const metadata = isCloudInstanceDiscoveryResponse(response.body) ? response.body.metadata : [];
if (metadata.length === 0) {
// If no metadata is returned, authority is untrusted
Expand Down
8 changes: 4 additions & 4 deletions lib/msal-common/src/authority/AuthorityFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ export class AuthorityFactory {
* @param networkClient
* @param protocolMode
*/
static async createDiscoveredInstance(authorityUri: string, networkClient: INetworkModule, cacheManager: ICacheManager, authorityOptions: AuthorityOptions): Promise<Authority> {
static async createDiscoveredInstance(authorityUri: string, networkClient: INetworkModule, cacheManager: ICacheManager, authorityOptions: AuthorityOptions, proxyUrl?: string): Promise<Authority> {
// Initialize authority and perform discovery endpoint check.
const acquireTokenAuthority: Authority = AuthorityFactory.createInstance(authorityUri, networkClient, cacheManager, authorityOptions);
const acquireTokenAuthority: Authority = AuthorityFactory.createInstance(authorityUri, networkClient, cacheManager, authorityOptions, proxyUrl);

try {
await acquireTokenAuthority.resolveEndpointsAsync();
Expand All @@ -45,12 +45,12 @@ export class AuthorityFactory {
* @param networkInterface
* @param protocolMode
*/
static createInstance(authorityUrl: string, networkInterface: INetworkModule, cacheManager: ICacheManager, authorityOptions: AuthorityOptions): Authority {
static createInstance(authorityUrl: string, networkInterface: INetworkModule, cacheManager: ICacheManager, authorityOptions: AuthorityOptions, proxyUrl?: string): Authority {
// Throw error if authority url is empty
if (StringUtils.isEmpty(authorityUrl)) {
throw ClientConfigurationError.createUrlEmptyError();
}

return new Authority(authorityUrl, networkInterface, cacheManager, authorityOptions);
return new Authority(authorityUrl, networkInterface, cacheManager, authorityOptions, proxyUrl);
}
}
11 changes: 11 additions & 0 deletions lib/msal-common/src/authority/ImdsOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

export type ImdsOptions = {
headers?: {
Metadata: string,
},
proxyUrl?: string,
};
28 changes: 19 additions & 9 deletions lib/msal-common/src/authority/RegionDiscovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ import { NetworkResponse } from "../network/NetworkManager";
import { IMDSBadResponse } from "../response/IMDSBadResponse";
import { Constants, RegionDiscoverySources, ResponseCodes } from "../utils/Constants";
import { RegionDiscoveryMetadata } from "./RegionDiscoveryMetadata";
import { ImdsOptions } from "./ImdsOptions";

export class RegionDiscovery {
// Network interface to make requests with.
protected networkInterface: INetworkModule;
// Options for the IMDS endpoint request
protected static IMDS_OPTIONS = {headers: {"Metadata": "true"}};
protected static IMDS_OPTIONS: ImdsOptions = {
headers: {
Metadata: "true",
},
};

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

// Check if a region was detected from the environment, if not, attempt to get the region from IMDS
if (!autodetectedRegionName) {
const options = RegionDiscovery.IMDS_OPTIONS;
if (proxyUrl) {
options.proxyUrl = proxyUrl;
}

try {
const localIMDSVersionResponse = await this.getRegionFromIMDS(Constants.IMDS_VERSION);
const localIMDSVersionResponse = await this.getRegionFromIMDS(Constants.IMDS_VERSION, options);
if (localIMDSVersionResponse.status === ResponseCodes.httpSuccess) {
autodetectedRegionName = localIMDSVersionResponse.body;
regionDiscoveryMetadata.region_source = RegionDiscoverySources.IMDS;
}

// If the response using the local IMDS version failed, try to fetch the current version of IMDS and retry.
if (localIMDSVersionResponse.status === ResponseCodes.httpBadRequest) {
const currentIMDSVersion = await this.getCurrentVersion();
const currentIMDSVersion = await this.getCurrentVersion(options);
if (!currentIMDSVersion) {
regionDiscoveryMetadata.region_source = RegionDiscoverySources.FAILED_AUTO_DETECTION;
return null;
}

const currentIMDSVersionResponse = await this.getRegionFromIMDS(currentIMDSVersion);
const currentIMDSVersionResponse = await this.getRegionFromIMDS(currentIMDSVersion, options);
if (currentIMDSVersionResponse.status === ResponseCodes.httpSuccess) {
autodetectedRegionName = currentIMDSVersionResponse.body;
regionDiscoveryMetadata.region_source = RegionDiscoverySources.IMDS;
Expand Down Expand Up @@ -73,18 +83,18 @@ export class RegionDiscovery {
* @param imdsEndpointUrl
* @returns Promise<NetworkResponse<string>>
*/
private async getRegionFromIMDS(version: string): Promise<NetworkResponse<string>> {
return this.networkInterface.sendGetRequestAsync<string>(`${Constants.IMDS_ENDPOINT}?api-version=${version}&format=text`, RegionDiscovery.IMDS_OPTIONS, Constants.IMDS_TIMEOUT);
private async getRegionFromIMDS(version: string, options: ImdsOptions): Promise<NetworkResponse<string>> {
return this.networkInterface.sendGetRequestAsync<string>(`${Constants.IMDS_ENDPOINT}?api-version=${version}&format=text`, options, Constants.IMDS_TIMEOUT);
}

/**
* Get the most recent version of the IMDS endpoint available
*
* @returns Promise<string | null>
*/
private async getCurrentVersion(): Promise<string | null> {
private async getCurrentVersion(options: ImdsOptions): Promise<string | null> {
try {
const response = await this.networkInterface.sendGetRequestAsync<IMDSBadResponse>(`${Constants.IMDS_ENDPOINT}?format=json`, RegionDiscovery.IMDS_OPTIONS);
const response = await this.networkInterface.sendGetRequestAsync<IMDSBadResponse>(`${Constants.IMDS_ENDPOINT}?format=json`, options);

// When IMDS endpoint is called without the api version query param, bad request response comes back with latest version.
if (response.status === ResponseCodes.httpBadRequest && response.body && response.body["newest-versions"] && response.body["newest-versions"].length > 0) {
Expand Down
4 changes: 2 additions & 2 deletions lib/msal-common/src/client/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export abstract class BaseClient {
const response = await this.networkManager.sendPostRequest<ServerAuthorizationTokenResponse>(
thumbprint,
tokenEndpoint,
{ body: queryString, headers: headers }
{ body: queryString, headers: headers, proxyUrl: this.config.systemOptions.proxyUrl }
);

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

/**
* Updates the authority object of the client. Endpoint discovery must be completed.
* @param updatedAuthority
* @param updatedAuthority
*/
updateAuthority(updatedAuthority: Authority): void {
if (!updatedAuthority.discoveryComplete()) {
Expand Down
3 changes: 2 additions & 1 deletion lib/msal-common/src/client/DeviceCodeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ export class DeviceCodeClient extends BaseClient {
deviceCodeEndpoint,
{
body: queryString,
headers: headers
headers: headers,
proxyUrl: this.config.systemOptions.proxyUrl
});

return {
Expand Down
4 changes: 3 additions & 1 deletion lib/msal-common/src/config/ClientConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export type AuthOptions = {
export type SystemOptions = {
tokenRenewalOffsetSeconds?: number;
preventCorsPreflight?: boolean;
proxyUrl?: string;
};

/**
Expand Down Expand Up @@ -136,7 +137,8 @@ export type AzureCloudOptions = {

export const DEFAULT_SYSTEM_OPTIONS: Required<SystemOptions> = {
tokenRenewalOffsetSeconds: DEFAULT_TOKEN_RENEWAL_OFFSET_SEC,
preventCorsPreflight: false
preventCorsPreflight: false,
proxyUrl: "",
};

const DEFAULT_LOGGER_IMPLEMENTATION: Required<LoggerOptions> = {
Expand Down
1 change: 1 addition & 0 deletions lib/msal-common/src/network/INetworkModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { NetworkResponse } from "./NetworkManager";
export type NetworkRequestOptions = {
headers?: Record<string, string>,
body?: string;
proxyUrl?: string;
};

/**
Expand Down
16 changes: 8 additions & 8 deletions lib/msal-common/src/url/UrlString.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class UrlString {
public get urlString(): string {
return this._urlString;
}

constructor(url: string) {
this._urlString = url;
if (StringUtils.isEmpty(this._urlString)) {
Expand All @@ -35,7 +35,7 @@ export class UrlString {

/**
* Ensure urls are lower case and end with a / character.
* @param url
* @param url
*/
static canonicalizeUri(url: string): string {
if (url) {
Expand Down Expand Up @@ -82,8 +82,8 @@ export class UrlString {

/**
* Given a url and a query string return the url with provided query string appended
* @param url
* @param queryString
* @param url
* @param queryString
*/
static appendQueryString(url: string, queryString: string): string {
if (StringUtils.isEmpty(queryString)) {
Expand All @@ -95,7 +95,7 @@ export class UrlString {

/**
* Returns a url with the hash removed
* @param url
* @param url
*/
static removeHashFromUrl(url: string): string {
return UrlString.canonicalizeUri(url.split("#")[0]);
Expand Down Expand Up @@ -173,13 +173,13 @@ export class UrlString {

return baseComponents.Protocol + "//" + baseComponents.HostNameAndPort + relativeUrl;
}

return relativeUrl;
}

/**
* Parses hash string from given string. Returns empty string if no hash symbol is found.
* @param hashString
* @param hashString
*/
static parseHash(hashString: string): string {
const hashIndex1 = hashString.indexOf("#");
Expand Down
4 changes: 3 additions & 1 deletion lib/msal-node/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ const msalConfig = {
},
piiLoggingEnabled: false,
logLevel: msal.LogLevel.Verbose,
}
},
proxyUrl: "",
}
}

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

### Logger Config Options
| Option | Description | Format | Default Value |
Expand Down
3 changes: 3 additions & 0 deletions lib/msal-node/docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ If you want to work around this, please note:
### How do I implement self-service sign-up with MSAL Node?
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.

### Why doesn't my app function correctly when it's running behind a proxy?
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.

## B2C

### How do I handle the password-reset user-flow?
Expand Down
17 changes: 17 additions & 0 deletions lib/msal-node/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/msal-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"dependencies": {
"@azure/msal-common": "^6.0.0",
"axios": "^0.21.4",
"https-proxy-agent": "^5.0.0",
"jsonwebtoken": "^8.5.1",
"uuid": "^8.3.0"
},
Expand Down
4 changes: 4 additions & 0 deletions lib/msal-node/src/client/ClientApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ export abstract class ClientApplication {
authority: discoveredAuthority,
clientCapabilities: this.config.auth.clientCapabilities
},
systemOptions: {
proxyUrl: this.config.system.proxyUrl,
},
loggerOptions: {
logLevel: this.config.system.loggerOptions.logLevel,
loggerCallback: this.config.system.loggerOptions
Expand Down Expand Up @@ -407,6 +410,7 @@ export abstract class ClientApplication {
authorityMetadata: this.config.auth.authorityMetadata,
azureRegionConfiguration
};

return await AuthorityFactory.createDiscoveredInstance(authorityUrl, this.config.system.networkClient, this.storage, authorityOptions);
}
}

0 comments on commit 639253a

Please sign in to comment.