wangzhibo
2026-05-09 53418655c061ac26f66058253f39200aced1ab1d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
/*
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License.
 */
 
import {
    AuthError,
    ClientAuthErrorCodes,
    createClientAuthError,
    INetworkModule,
    NetworkResponse,
    NetworkRequestOptions,
    Logger,
    ServerAuthorizationTokenResponse,
    Constants,
} from "@azure/msal-common/node";
import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js";
import { BaseManagedIdentitySource } from "./BaseManagedIdentitySource.js";
import { CryptoProvider } from "../../crypto/CryptoProvider.js";
import {
    ManagedIdentityErrorCodes,
    createManagedIdentityError,
} from "../../error/ManagedIdentityError.js";
import {
    AZURE_ARC_SECRET_FILE_MAX_SIZE_BYTES,
    HttpMethod,
    ManagedIdentityEnvironmentVariableNames,
    ManagedIdentityHeaders,
    ManagedIdentityIdType,
    ManagedIdentityQueryParameters,
    ManagedIdentitySourceNames,
} from "../../utils/Constants.js";
import { NodeStorage } from "../../cache/NodeStorage.js";
import {
    accessSync,
    constants as fsConstants,
    readFileSync,
    statSync,
} from "fs";
import { ManagedIdentityTokenResponse } from "../../response/ManagedIdentityTokenResponse.js";
import { ManagedIdentityId } from "../../config/ManagedIdentityId.js";
import path from "path";
 
export const ARC_API_VERSION: string = "2019-11-01";
export const DEFAULT_AZURE_ARC_IDENTITY_ENDPOINT: string =
    "http://127.0.0.1:40342/metadata/identity/oauth2/token";
const HIMDS_EXECUTABLE_HELPER_STRING = "N/A: himds executable exists";
 
type FilePathMap = {
    win32: string;
    linux: string;
};
 
export const SUPPORTED_AZURE_ARC_PLATFORMS: FilePathMap = {
    win32: `${process.env["ProgramData"]}\\AzureConnectedMachineAgent\\Tokens\\`,
    linux: "/var/opt/azcmagent/tokens/",
};
 
export const AZURE_ARC_FILE_DETECTION: FilePathMap = {
    win32: `${process.env["ProgramFiles"]}\\AzureConnectedMachineAgent\\himds.exe`,
    linux: "/opt/azcmagent/bin/himds",
};
 
/**
 * Azure Arc managed identity source implementation for acquiring tokens from Azure Arc-enabled servers.
 *
 * This class provides managed identity authentication for applications running on Azure Arc-enabled servers
 * by communicating with the local Hybrid Instance Metadata Service (HIMDS). It supports both environment
 * variable-based configuration and automatic detection through the HIMDS executable.
 *
 * Original source of code: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/src/AzureArcManagedIdentitySource.cs
 */
export class AzureArc extends BaseManagedIdentitySource {
    private identityEndpoint: string;
 
    /**
     * Creates a new instance of the AzureArc managed identity source.
     *
     * @param logger - Logger instance for capturing telemetry and diagnostic information
     * @param nodeStorage - Storage implementation for caching tokens and metadata
     * @param networkClient - Network client for making HTTP requests to the identity endpoint
     * @param cryptoProvider - Cryptographic operations provider for token validation and encryption
     * @param disableInternalRetries - Flag to disable automatic retry logic for failed requests
     * @param identityEndpoint - The Azure Arc identity endpoint URL for token requests
     */
    constructor(
        logger: Logger,
        nodeStorage: NodeStorage,
        networkClient: INetworkModule,
        cryptoProvider: CryptoProvider,
        disableInternalRetries: boolean,
        identityEndpoint: string
    ) {
        super(
            logger,
            nodeStorage,
            networkClient,
            cryptoProvider,
            disableInternalRetries
        );
 
        this.identityEndpoint = identityEndpoint;
    }
 
    /**
     * Retrieves and validates Azure Arc environment variables for managed identity configuration.
     *
     * This method checks for IDENTITY_ENDPOINT and IMDS_ENDPOINT environment variables.
     * If either is missing, it attempts to detect the Azure Arc environment by checking for
     * the HIMDS executable at platform-specific paths. On successful detection, it returns
     * the default identity endpoint and a helper string indicating file-based detection.
     *
     * @returns An array containing [identityEndpoint, imdsEndpoint] where both values are
     *          strings if Azure Arc is available, or undefined if not available.
     */
    public static getEnvironmentVariables(): Array<string | undefined> {
        let identityEndpoint: string | undefined =
            process.env[
                ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT
            ];
        let imdsEndpoint: string | undefined =
            process.env[ManagedIdentityEnvironmentVariableNames.IMDS_ENDPOINT];
 
        // if either of the identity or imds endpoints are undefined, check if the himds executable exists
        if (!identityEndpoint || !imdsEndpoint) {
            // get the expected Windows or Linux file path of the himds executable
            const fileDetectionPath: string =
                AZURE_ARC_FILE_DETECTION[process.platform as keyof FilePathMap];
            try {
                /*
                 * check if the himds executable exists and its permissions allow it to be read
                 * returns undefined if true, throws an error otherwise
                 */
                accessSync(
                    fileDetectionPath,
                    fsConstants.F_OK | fsConstants.R_OK
                );
 
                identityEndpoint = DEFAULT_AZURE_ARC_IDENTITY_ENDPOINT;
                imdsEndpoint = HIMDS_EXECUTABLE_HELPER_STRING;
            } catch (err) {
                /*
                 * do nothing
                 * accessSync returns undefined on success, and throws an error on failure
                 */
            }
        }
 
        return [identityEndpoint, imdsEndpoint];
    }
 
    /**
     * Attempts to create an AzureArc managed identity source instance.
     *
     * Validates the Azure Arc environment by checking environment variables
     * and performing file-based detection. It ensures that only system-assigned managed identities
     * are supported for Azure Arc scenarios. The method performs comprehensive validation of
     * endpoint URLs and logs detailed information about the detection process.
     *
     * @param logger - Logger instance for capturing creation and validation steps
     * @param nodeStorage - Storage implementation for the managed identity source
     * @param networkClient - Network client for HTTP communication
     * @param cryptoProvider - Cryptographic operations provider
     * @param disableInternalRetries - Whether to disable automatic retry mechanisms
     * @param managedIdentityId - The managed identity configuration, must be system-assigned
     *
     * @returns AzureArc instance if the environment supports Azure Arc managed identity, null otherwise
     *
     * @throws {ManagedIdentityError} When a user-assigned managed identity is specified (not supported for Azure Arc)
     */
    public static tryCreate(
        logger: Logger,
        nodeStorage: NodeStorage,
        networkClient: INetworkModule,
        cryptoProvider: CryptoProvider,
        disableInternalRetries: boolean,
        managedIdentityId: ManagedIdentityId
    ): AzureArc | null {
        const [identityEndpoint, imdsEndpoint] =
            AzureArc.getEnvironmentVariables();
 
        // if either of the identity or imds endpoints are undefined (even after himds file detection)
        if (!identityEndpoint || !imdsEndpoint) {
            logger.info(
                `[Managed Identity] ${ManagedIdentitySourceNames.AZURE_ARC} managed identity is unavailable through environment variables because one or both of '${ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT}' and '${ManagedIdentityEnvironmentVariableNames.IMDS_ENDPOINT}' are not defined. ${ManagedIdentitySourceNames.AZURE_ARC} managed identity is also unavailable through file detection.`,
                ""
            );
 
            return null;
        }
 
        // check if the imds endpoint is set to the default for file detection
        if (imdsEndpoint === HIMDS_EXECUTABLE_HELPER_STRING) {
            logger.info(
                `[Managed Identity] ${ManagedIdentitySourceNames.AZURE_ARC} managed identity is available through file detection. Defaulting to known ${ManagedIdentitySourceNames.AZURE_ARC} endpoint: ${DEFAULT_AZURE_ARC_IDENTITY_ENDPOINT}. Creating ${ManagedIdentitySourceNames.AZURE_ARC} managed identity.`,
                ""
            );
        } else {
            // otherwise, both the identity and imds endpoints are defined without file detection; validate them
 
            const validatedIdentityEndpoint: string =
                AzureArc.getValidatedEnvVariableUrlString(
                    ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT,
                    identityEndpoint,
                    ManagedIdentitySourceNames.AZURE_ARC,
                    logger
                );
            // remove trailing slash
            validatedIdentityEndpoint.endsWith("/")
                ? validatedIdentityEndpoint.slice(0, -1)
                : validatedIdentityEndpoint;
 
            AzureArc.getValidatedEnvVariableUrlString(
                ManagedIdentityEnvironmentVariableNames.IMDS_ENDPOINT,
                imdsEndpoint,
                ManagedIdentitySourceNames.AZURE_ARC,
                logger
            );
 
            logger.info(
                `[Managed Identity] Environment variables validation passed for ${ManagedIdentitySourceNames.AZURE_ARC} managed identity. Endpoint URI: ${validatedIdentityEndpoint}. Creating ${ManagedIdentitySourceNames.AZURE_ARC} managed identity.`,
                ""
            );
        }
 
        if (
            managedIdentityId.idType !== ManagedIdentityIdType.SYSTEM_ASSIGNED
        ) {
            throw createManagedIdentityError(
                ManagedIdentityErrorCodes.unableToCreateAzureArc
            );
        }
 
        return new AzureArc(
            logger,
            nodeStorage,
            networkClient,
            cryptoProvider,
            disableInternalRetries,
            identityEndpoint
        );
    }
 
    /**
     * Creates a properly formatted HTTP request for acquiring tokens from the Azure Arc identity endpoint.
     *
     * This method constructs a GET request to the Azure Arc HIMDS endpoint with the required metadata header
     * and query parameters. The endpoint URL is normalized to use 127.0.0.1 instead of localhost for
     * consistency. Additional body parameters are calculated by the base class during token acquisition.
     *
     * @param resource - The target resource/scope for which to request an access token (e.g., "https://graph.microsoft.com/.default")
     *
     * @returns A configured ManagedIdentityRequestParameters object ready for network execution
     */
    public createRequest(resource: string): ManagedIdentityRequestParameters {
        const request: ManagedIdentityRequestParameters =
            new ManagedIdentityRequestParameters(
                HttpMethod.GET,
                this.identityEndpoint.replace("localhost", "127.0.0.1")
            );
 
        request.headers[ManagedIdentityHeaders.METADATA_HEADER_NAME] = "true";
 
        request.queryParameters[ManagedIdentityQueryParameters.API_VERSION] =
            ARC_API_VERSION;
        request.queryParameters[ManagedIdentityQueryParameters.RESOURCE] =
            resource;
 
        // bodyParameters calculated in BaseManagedIdentity.acquireTokenWithManagedIdentity
 
        return request;
    }
 
    /**
     * Processes the server response and handles Azure Arc-specific authentication challenges.
     *
     * This method implements the Azure Arc authentication flow which may require reading a secret file
     * for authorization. When the initial request returns HTTP 401 Unauthorized, it extracts the file
     * path from the WWW-Authenticate header, validates the file location and size, reads the secret,
     * and retries the request with Basic authentication. The method includes comprehensive security
     * validations to prevent path traversal and ensure file integrity.
     *
     * @param originalResponse - The initial HTTP response from the identity endpoint
     * @param networkClient - Network client for making the retry request if needed
     * @param networkRequest - The original request parameters (modified with auth header for retry)
     * @param networkRequestOptions - Additional options for network requests
     *
     * @returns A promise that resolves to the server token response with access token and metadata
     *
     * @throws {ManagedIdentityError} When:
     *   - WWW-Authenticate header is missing or has unsupported format
     *   - Platform is not supported (not Windows or Linux)
     *   - Secret file has invalid extension (not .key)
     *   - Secret file path doesn't match expected platform path
     *   - Secret file cannot be read or is too large (>4096 bytes)
     * @throws {ClientAuthError} When network errors occur during retry request
     */
    public async getServerTokenResponseAsync(
        originalResponse: NetworkResponse<ManagedIdentityTokenResponse>,
        networkClient: INetworkModule,
        networkRequest: ManagedIdentityRequestParameters,
        networkRequestOptions: NetworkRequestOptions
    ): Promise<ServerAuthorizationTokenResponse> {
        let retryResponse:
            | NetworkResponse<ManagedIdentityTokenResponse>
            | undefined;
 
        if (originalResponse.status === Constants.HTTP_UNAUTHORIZED) {
            const wwwAuthHeader: string =
                originalResponse.headers["www-authenticate"];
            if (!wwwAuthHeader) {
                throw createManagedIdentityError(
                    ManagedIdentityErrorCodes.wwwAuthenticateHeaderMissing
                );
            }
            if (!wwwAuthHeader.includes("Basic realm=")) {
                throw createManagedIdentityError(
                    ManagedIdentityErrorCodes.wwwAuthenticateHeaderUnsupportedFormat
                );
            }
 
            const secretFilePath = wwwAuthHeader.split("Basic realm=")[1];
 
            // throw an error if the managed identity application is not being run on Windows or Linux
            if (
                !SUPPORTED_AZURE_ARC_PLATFORMS.hasOwnProperty(process.platform)
            ) {
                throw createManagedIdentityError(
                    ManagedIdentityErrorCodes.platformNotSupported
                );
            }
 
            // get the expected Windows or Linux file path
            const expectedSecretFilePath: string =
                SUPPORTED_AZURE_ARC_PLATFORMS[
                    process.platform as keyof FilePathMap
                ];
 
            // throw an error if the file in the file path is not a .key file
            const fileName: string = path.basename(secretFilePath);
            if (!fileName.endsWith(".key")) {
                throw createManagedIdentityError(
                    ManagedIdentityErrorCodes.invalidFileExtension
                );
            }
 
            /*
             * throw an error if the file path from the www-authenticate header does not match the
             * expected file path for the platform (Windows or Linux) the managed identity application
             * is running on
             */
            if (expectedSecretFilePath + fileName !== secretFilePath) {
                throw createManagedIdentityError(
                    ManagedIdentityErrorCodes.invalidFilePath
                );
            }
 
            let secretFileSize;
            // attempt to get the secret file's size, in bytes
            try {
                secretFileSize = await statSync(secretFilePath).size;
            } catch (e) {
                throw createManagedIdentityError(
                    ManagedIdentityErrorCodes.unableToReadSecretFile
                );
            }
            // throw an error if the secret file's size is greater than 4096 bytes
            if (secretFileSize > AZURE_ARC_SECRET_FILE_MAX_SIZE_BYTES) {
                throw createManagedIdentityError(
                    ManagedIdentityErrorCodes.invalidSecret
                );
            }
 
            // attempt to read the contents of the secret file
            let secret;
            try {
                secret = readFileSync(
                    secretFilePath,
                    Constants.EncodingTypes.UTF8
                );
            } catch (e) {
                throw createManagedIdentityError(
                    ManagedIdentityErrorCodes.unableToReadSecretFile
                );
            }
            const authHeaderValue = `Basic ${secret}`;
 
            this.logger.info(
                `[Managed Identity] Adding authorization header to the request.`,
                ""
            );
            networkRequest.headers[
                ManagedIdentityHeaders.AUTHORIZATION_HEADER_NAME
            ] = authHeaderValue;
 
            try {
                retryResponse =
                    await networkClient.sendGetRequestAsync<ManagedIdentityTokenResponse>(
                        networkRequest.computeUri(),
                        networkRequestOptions
                    );
            } catch (error) {
                if (error instanceof AuthError) {
                    throw error;
                } else {
                    throw createClientAuthError(
                        ClientAuthErrorCodes.networkError
                    );
                }
            }
        }
 
        return this.getServerTokenResponse(retryResponse || originalResponse);
    }
}