Skip to content

Commit af94b11

Browse files
Implemented a Retry Policy for the IMDS Managed Identity Source (#7614)
The IMDS retry policy spec is located [here](https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/blob/main/docs/imds_retry_based_on_errors.md). The existing (linear) retry policy tests were moved out of Imds.spec.ts and into a new file, DefaultManagedIdentityRetryPolicy.spec.ts.
1 parent 0b5bd39 commit af94b11

18 files changed

+967
-236
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Implemented a Retry Policy for the IMDS Managed Identity Source #7614",
4+
"packageName": "@azure/msal-common",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Implemented a Retry Policy for the IMDS Managed Identity Source #7614",
4+
"packageName": "@azure/msal-node",
5+
"email": "[email protected]",
6+
"dependentChangeType": "patch"
7+
}

lib/msal-common/apiReview/msal-common.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2412,6 +2412,7 @@ export const HttpStatus: {
24122412
readonly UNAUTHORIZED: 401;
24132413
readonly NOT_FOUND: 404;
24142414
readonly REQUEST_TIMEOUT: 408;
2415+
readonly GONE: 410;
24152416
readonly TOO_MANY_REQUESTS: 429;
24162417
readonly CLIENT_ERROR_RANGE_END: 499;
24172418
readonly SERVER_ERROR: 500;

lib/msal-common/src/utils/Constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export const HttpStatus = {
7070
UNAUTHORIZED: 401,
7171
NOT_FOUND: 404,
7272
REQUEST_TIMEOUT: 408,
73+
GONE: 410,
7374
TOO_MANY_REQUESTS: 429,
7475
CLIENT_ERROR_RANGE_END: 499,
7576
SERVER_ERROR: 500,

lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ export abstract class BaseManagedIdentitySource {
164164
? this.networkClient
165165
: new HttpClientWithRetries(
166166
this.networkClient,
167-
networkRequest.retryPolicy
167+
networkRequest.retryPolicy,
168+
this.logger
168169
);
169170

170171
const reqTimestamp = TimeUtils.nowSeconds();

lib/msal-node/src/client/ManagedIdentitySources/Imds.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
RESOURCE_BODY_OR_QUERY_PARAMETER_NAME,
1919
} from "../../utils/Constants.js";
2020
import { NodeStorage } from "../../cache/NodeStorage.js";
21+
import { ImdsRetryPolicy } from "../../retry/ImdsRetryPolicy.js";
2122

2223
// IMDS constants. Docs for IMDS are available here https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http
2324
const IMDS_TOKEN_PATH: string = "/metadata/identity/oauth2/token";
@@ -131,6 +132,8 @@ export class Imds extends BaseManagedIdentitySource {
131132

132133
// bodyParameters calculated in BaseManagedIdentity.acquireTokenWithManagedIdentity
133134

135+
request.retryPolicy = new ImdsRetryPolicy();
136+
134137
return request;
135138
}
136139
}

lib/msal-node/src/config/ManagedIdentityRequestParameters.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,8 @@ import {
88
UrlString,
99
UrlUtils,
1010
} from "@azure/msal-common/node";
11-
import {
12-
HttpMethod,
13-
MANAGED_IDENTITY_HTTP_STATUS_CODES_TO_RETRY_ON,
14-
MANAGED_IDENTITY_MAX_RETRIES,
15-
MANAGED_IDENTITY_RETRY_DELAY,
16-
RetryPolicies,
17-
} from "../utils/Constants.js";
18-
import { LinearRetryPolicy } from "../retry/LinearRetryPolicy.js";
11+
import { DefaultManagedIdentityRetryPolicy } from "../retry/DefaultManagedIdentityRetryPolicy.js";
12+
import { HttpMethod, RetryPolicies } from "../utils/Constants.js";
1913

2014
export class ManagedIdentityRequestParameters {
2115
private _baseEndpoint: string;
@@ -36,12 +30,8 @@ export class ManagedIdentityRequestParameters {
3630
this.bodyParameters = {} as Record<string, string>;
3731
this.queryParameters = {} as Record<string, string>;
3832

39-
const defaultRetryPolicy: LinearRetryPolicy = new LinearRetryPolicy(
40-
MANAGED_IDENTITY_MAX_RETRIES,
41-
MANAGED_IDENTITY_RETRY_DELAY,
42-
MANAGED_IDENTITY_HTTP_STATUS_CODES_TO_RETRY_ON
43-
);
44-
this.retryPolicy = retryPolicy || defaultRetryPolicy;
33+
this.retryPolicy =
34+
retryPolicy || new DefaultManagedIdentityRetryPolicy();
4535
}
4636

4737
public computeUri(): string {

lib/msal-node/src/network/HttpClientWithRetries.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import {
77
HeaderNames,
88
INetworkModule,
9+
Logger,
910
NetworkRequestOptions,
1011
NetworkResponse,
1112
} from "@azure/msal-common/node";
@@ -15,13 +16,16 @@ import { HttpMethod } from "../utils/Constants.js";
1516
export class HttpClientWithRetries implements INetworkModule {
1617
private httpClientNoRetries: INetworkModule;
1718
private retryPolicy: IHttpRetryPolicy;
19+
private logger: Logger;
1820

1921
constructor(
2022
httpClientNoRetries: INetworkModule,
21-
retryPolicy: IHttpRetryPolicy
23+
retryPolicy: IHttpRetryPolicy,
24+
logger: Logger
2225
) {
2326
this.httpClientNoRetries = httpClientNoRetries;
2427
this.retryPolicy = retryPolicy;
28+
this.logger = logger;
2529
}
2630

2731
private async sendNetworkRequestAsyncHelper<T>(
@@ -45,11 +49,16 @@ export class HttpClientWithRetries implements INetworkModule {
4549
let response: NetworkResponse<T> =
4650
await this.sendNetworkRequestAsyncHelper(httpMethod, url, options);
4751

52+
if ("isNewRequest" in this.retryPolicy) {
53+
this.retryPolicy.isNewRequest = true;
54+
}
55+
4856
let currentRetry: number = 0;
4957
while (
5058
await this.retryPolicy.pauseForRetry(
5159
response.status,
5260
currentRetry,
61+
this.logger,
5362
response.headers[HeaderNames.RETRY_AFTER]
5463
)
5564
) {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
import { IncomingHttpHeaders } from "http";
7+
import { HttpStatus, Logger } from "@azure/msal-common";
8+
import { IHttpRetryPolicy } from "./IHttpRetryPolicy.js";
9+
import { LinearRetryStrategy } from "./LinearRetryStrategy.js";
10+
11+
const DEFAULT_MANAGED_IDENTITY_MAX_RETRIES: number = 3;
12+
const DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS: number = 1000;
13+
const DEFAULT_MANAGED_IDENTITY_HTTP_STATUS_CODES_TO_RETRY_ON: Array<number> = [
14+
HttpStatus.NOT_FOUND,
15+
HttpStatus.REQUEST_TIMEOUT,
16+
HttpStatus.TOO_MANY_REQUESTS,
17+
HttpStatus.SERVER_ERROR,
18+
HttpStatus.SERVICE_UNAVAILABLE,
19+
HttpStatus.GATEWAY_TIMEOUT,
20+
];
21+
22+
export class DefaultManagedIdentityRetryPolicy implements IHttpRetryPolicy {
23+
/*
24+
* this is defined here as a static variable despite being defined as a constant outside of the
25+
* class because it needs to be overridden in the unit tests so that the unit tests run faster
26+
*/
27+
static get DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS(): number {
28+
return DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS;
29+
}
30+
31+
private linearRetryStrategy: LinearRetryStrategy =
32+
new LinearRetryStrategy();
33+
34+
async pauseForRetry(
35+
httpStatusCode: number,
36+
currentRetry: number,
37+
logger: Logger,
38+
retryAfterHeader: IncomingHttpHeaders["retry-after"]
39+
): Promise<boolean> {
40+
if (
41+
DEFAULT_MANAGED_IDENTITY_HTTP_STATUS_CODES_TO_RETRY_ON.includes(
42+
httpStatusCode
43+
) &&
44+
currentRetry < DEFAULT_MANAGED_IDENTITY_MAX_RETRIES
45+
) {
46+
const retryAfterDelay: number =
47+
this.linearRetryStrategy.calculateDelay(
48+
retryAfterHeader,
49+
DefaultManagedIdentityRetryPolicy.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS
50+
);
51+
52+
logger.verbose(
53+
`Retrying request in ${retryAfterDelay}ms (retry attempt: ${
54+
currentRetry + 1
55+
})`
56+
);
57+
58+
// pause execution for the calculated delay
59+
await new Promise((resolve) => {
60+
// retryAfterHeader value of 0 evaluates to false, and DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS will be used
61+
return setTimeout(resolve, retryAfterDelay);
62+
});
63+
64+
return true;
65+
}
66+
67+
// if the status code is not retriable or max retries have been reached, do not retry
68+
return false;
69+
}
70+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License.
4+
*/
5+
6+
export class ExponentialRetryStrategy {
7+
// Minimum backoff time in milliseconds
8+
private minExponentialBackoff: number;
9+
// Maximum backoff time in milliseconds
10+
private maxExponentialBackoff: number;
11+
// Maximum backoff time in milliseconds
12+
private exponentialDeltaBackoff: number;
13+
14+
constructor(
15+
minExponentialBackoff: number,
16+
maxExponentialBackoff: number,
17+
exponentialDeltaBackoff: number
18+
) {
19+
this.minExponentialBackoff = minExponentialBackoff;
20+
this.maxExponentialBackoff = maxExponentialBackoff;
21+
this.exponentialDeltaBackoff = exponentialDeltaBackoff;
22+
}
23+
24+
/**
25+
* Calculates the exponential delay based on the current retry attempt.
26+
*
27+
* @param {number} currentRetry - The current retry attempt number.
28+
* @returns {number} - The calculated exponential delay in milliseconds.
29+
*
30+
* The delay is calculated using the formula:
31+
* - If `currentRetry` is 0, it returns the minimum backoff time.
32+
* - Otherwise, it calculates the delay as the minimum of:
33+
* - `(2^(currentRetry - 1)) * deltaBackoff`
34+
* - `maxBackoff`
35+
*
36+
* This ensures that the delay increases exponentially with each retry attempt,
37+
* but does not exceed the maximum backoff time.
38+
*/
39+
public calculateDelay(currentRetry: number): number {
40+
// Attempt 1
41+
if (currentRetry === 0) {
42+
return this.minExponentialBackoff;
43+
}
44+
45+
// Attempt 2+
46+
const exponentialDelay = Math.min(
47+
Math.pow(2, currentRetry - 1) * this.exponentialDeltaBackoff,
48+
this.maxExponentialBackoff
49+
);
50+
51+
return exponentialDelay;
52+
}
53+
}

lib/msal-node/src/retry/IHttpRetryPolicy.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,25 @@
33
* Licensed under the MIT License.
44
*/
55

6-
import http from "http";
6+
import { IncomingHttpHeaders } from "http";
7+
import { Logger } from "@azure/msal-common";
78

89
export interface IHttpRetryPolicy {
9-
/*
10-
* if retry conditions occur, pauses and returns true
11-
* otherwise return false
10+
_isNewRequest?: boolean;
11+
// set isNewRequest(value: boolean);
12+
13+
/**
14+
* Pauses execution for a specified amount of time before retrying an HTTP request.
15+
*
16+
* @param httpStatusCode - The HTTP status code of the response.
17+
* @param currentRetry - The current retry attempt number.
18+
* @param retryAfterHeader - The value of the `retry-after` HTTP header, if present.
19+
* @returns A promise that resolves to a boolean indicating whether to retry the request.
1220
*/
1321
pauseForRetry(
1422
httpStatusCode: number,
1523
currentRetry: number,
16-
retryAfterHeader: http.IncomingHttpHeaders["retry-after"]
24+
logger: Logger,
25+
retryAfterHeader?: IncomingHttpHeaders["retry-after"]
1726
): Promise<boolean>;
1827
}

0 commit comments

Comments
 (0)