@azure/msal-node
Version:
Microsoft Authentication Library for Node
329 lines (290 loc) • 12.6 kB
text/typescript
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AuthError,
ClientAuthErrorCodes,
createClientAuthError,
HttpStatus,
INetworkModule,
NetworkResponse,
NetworkRequestOptions,
Logger,
ServerAuthorizationTokenResponse,
EncodingTypes,
} 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",
};
/**
* 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;
constructor(
logger: Logger,
nodeStorage: NodeStorage,
networkClient: INetworkModule,
cryptoProvider: CryptoProvider,
disableInternalRetries: boolean,
identityEndpoint: string
) {
super(
logger,
nodeStorage,
networkClient,
cryptoProvider,
disableInternalRetries
);
this.identityEndpoint = identityEndpoint;
}
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];
}
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
);
}
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;
}
public async getServerTokenResponseAsync(
originalResponse: NetworkResponse<ManagedIdentityTokenResponse>,
networkClient: INetworkModule,
networkRequest: ManagedIdentityRequestParameters,
networkRequestOptions: NetworkRequestOptions
): Promise<ServerAuthorizationTokenResponse> {
let retryResponse:
| NetworkResponse<ManagedIdentityTokenResponse>
| undefined;
if (originalResponse.status === HttpStatus.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, 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);
}
}