Add extracted source directory and README navigation

This commit is contained in:
Shawn Bot
2026-03-31 14:56:06 +00:00
parent 6252bb6eb5
commit 91e01d755b
4757 changed files with 984951 additions and 0 deletions

View File

@@ -0,0 +1,99 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { createHttpHeaders, createPipelineRequest } from "@azure/core-rest-pipeline";
import { isError } from "@azure/core-util";
import { credentialLogger } from "../../util/logging.js";
import { mapScopesToResource } from "./utils.js";
import { tracingClient } from "../../util/tracing.js";
const msiName = "ManagedIdentityCredential - IMDS";
const logger = credentialLogger(msiName);
const imdsHost = "http://169.254.169.254";
const imdsEndpointPath = "/metadata/identity/oauth2/token";
/**
* Generates an invalid request options to get a response quickly from IMDS endpoint.
* The response indicates the availability of IMSD service; otherwise the request would time out.
*/
function prepareInvalidRequestOptions(scopes) {
var _a;
const resource = mapScopesToResource(scopes);
if (!resource) {
throw new Error(`${msiName}: Multiple scopes are not supported.`);
}
// Pod Identity will try to process this request even if the Metadata header is missing.
// We can exclude the request query to ensure no IMDS endpoint tries to process the ping request.
const url = new URL(imdsEndpointPath, (_a = process.env.AZURE_POD_IDENTITY_AUTHORITY_HOST) !== null && _a !== void 0 ? _a : imdsHost);
const rawHeaders = {
Accept: "application/json",
// intentionally leave out the Metadata header to invoke an error from IMDS endpoint.
};
return {
// intentionally not including any query
url: `${url}`,
method: "GET",
headers: createHttpHeaders(rawHeaders),
};
}
/**
* Defines how to determine whether the Azure IMDS MSI is available.
*
* Actually getting the token once we determine IMDS is available is handled by MSAL.
*/
export const imdsMsi = {
name: "imdsMsi",
async isAvailable(options) {
const { scopes, identityClient, getTokenOptions } = options;
const resource = mapScopesToResource(scopes);
if (!resource) {
logger.info(`${msiName}: Unavailable. Multiple scopes are not supported.`);
return false;
}
// if the PodIdentityEndpoint environment variable was set no need to probe the endpoint, it can be assumed to exist
if (process.env.AZURE_POD_IDENTITY_AUTHORITY_HOST) {
return true;
}
if (!identityClient) {
throw new Error("Missing IdentityClient");
}
const requestOptions = prepareInvalidRequestOptions(resource);
return tracingClient.withSpan("ManagedIdentityCredential-pingImdsEndpoint", getTokenOptions !== null && getTokenOptions !== void 0 ? getTokenOptions : {}, async (updatedOptions) => {
var _a, _b;
requestOptions.tracingOptions = updatedOptions.tracingOptions;
// Create a request with a timeout since we expect that
// not having a "Metadata" header should cause an error to be
// returned quickly from the endpoint, proving its availability.
const request = createPipelineRequest(requestOptions);
// Default to 1000 if the default of 0 is used.
// Negative values can still be used to disable the timeout.
request.timeout = ((_a = updatedOptions.requestOptions) === null || _a === void 0 ? void 0 : _a.timeout) || 1000;
// This MSI uses the imdsEndpoint to get the token, which only uses http://
request.allowInsecureConnection = true;
let response;
try {
logger.info(`${msiName}: Pinging the Azure IMDS endpoint`);
response = await identityClient.sendRequest(request);
}
catch (err) {
// If the request failed, or Node.js was unable to establish a connection,
// or the host was down, we'll assume the IMDS endpoint isn't available.
if (isError(err)) {
logger.verbose(`${msiName}: Caught error ${err.name}: ${err.message}`);
}
// This is a special case for Docker Desktop which responds with a 403 with a message that contains "A socket operation was attempted to an unreachable network" or "A socket operation was attempted to an unreachable host"
// rather than just timing out, as expected.
logger.info(`${msiName}: The Azure IMDS endpoint is unavailable`);
return false;
}
if (response.status === 403) {
if ((_b = response.bodyAsText) === null || _b === void 0 ? void 0 : _b.includes("unreachable")) {
logger.info(`${msiName}: The Azure IMDS endpoint is unavailable`);
logger.info(`${msiName}: ${response.bodyAsText}`);
return false;
}
}
// If we received any response, the endpoint is available
logger.info(`${msiName}: The Azure IMDS endpoint is available`);
return true;
});
},
};
//# sourceMappingURL=imdsMsi.js.map

View File

@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { retryPolicy } from "@azure/core-rest-pipeline";
import { calculateRetryDelay } from "@azure/core-util";
// Matches the default retry configuration in expontentialRetryStrategy.ts
const DEFAULT_CLIENT_MAX_RETRY_INTERVAL = 1000 * 64;
/**
* An additional policy that retries on 404 errors. The default retry policy does not retry on
* 404s, but the IMDS endpoint can return 404s when the token is not yet available. This policy
* will retry on 404s with an exponential backoff.
*
* @param msiRetryConfig - The retry configuration for the MSI credential.
* @returns - The policy that will retry on 404s.
*/
export function imdsRetryPolicy(msiRetryConfig) {
return retryPolicy([
{
name: "imdsRetryPolicy",
retry: ({ retryCount, response }) => {
if ((response === null || response === void 0 ? void 0 : response.status) !== 404) {
return { skipStrategy: true };
}
return calculateRetryDelay(retryCount, {
retryDelayInMs: msiRetryConfig.startDelayInMs,
maxRetryDelayInMs: DEFAULT_CLIENT_MAX_RETRY_INTERVAL,
});
},
},
], {
maxRetries: msiRetryConfig.maxRetries,
});
}
//# sourceMappingURL=imdsRetryPolicy.js.map

View File

@@ -0,0 +1,239 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { getLogLevel } from "@azure/logger";
import { ManagedIdentityApplication } from "@azure/msal-node";
import { IdentityClient } from "../../client/identityClient.js";
import { AuthenticationRequiredError, CredentialUnavailableError } from "../../errors.js";
import { getMSALLogLevel, defaultLoggerCallback } from "../../msal/utils.js";
import { imdsRetryPolicy } from "./imdsRetryPolicy.js";
import { formatSuccess, formatError, credentialLogger } from "../../util/logging.js";
import { tracingClient } from "../../util/tracing.js";
import { imdsMsi } from "./imdsMsi.js";
import { tokenExchangeMsi } from "./tokenExchangeMsi.js";
import { mapScopesToResource, serviceFabricErrorMessage } from "./utils.js";
const logger = credentialLogger("ManagedIdentityCredential");
/**
* Attempts authentication using a managed identity available at the deployment environment.
* This authentication type works in Azure VMs, App Service instances, Azure Functions applications,
* Azure Kubernetes Services, Azure Service Fabric instances and inside of the Azure Cloud Shell.
*
* More information about configuring managed identities can be found here:
* https://learn.microsoft.com/azure/active-directory/managed-identities-azure-resources/overview
*/
export class ManagedIdentityCredential {
/**
* @internal
* @hidden
*/
constructor(clientIdOrOptions, options) {
var _a, _b;
this.msiRetryConfig = {
maxRetries: 5,
startDelayInMs: 800,
intervalIncrement: 2,
};
let _options;
if (typeof clientIdOrOptions === "string") {
this.clientId = clientIdOrOptions;
_options = options !== null && options !== void 0 ? options : {};
}
else {
this.clientId = clientIdOrOptions === null || clientIdOrOptions === void 0 ? void 0 : clientIdOrOptions.clientId;
_options = clientIdOrOptions !== null && clientIdOrOptions !== void 0 ? clientIdOrOptions : {};
}
this.resourceId = _options === null || _options === void 0 ? void 0 : _options.resourceId;
this.objectId = _options === null || _options === void 0 ? void 0 : _options.objectId;
// For JavaScript users.
const providedIds = [
{ key: "clientId", value: this.clientId },
{ key: "resourceId", value: this.resourceId },
{ key: "objectId", value: this.objectId },
].filter((id) => id.value);
if (providedIds.length > 1) {
throw new Error(`ManagedIdentityCredential: only one of 'clientId', 'resourceId', or 'objectId' can be provided. Received values: ${JSON.stringify({ clientId: this.clientId, resourceId: this.resourceId, objectId: this.objectId })}`);
}
// ManagedIdentity uses http for local requests
_options.allowInsecureConnection = true;
if (((_a = _options.retryOptions) === null || _a === void 0 ? void 0 : _a.maxRetries) !== undefined) {
this.msiRetryConfig.maxRetries = _options.retryOptions.maxRetries;
}
this.identityClient = new IdentityClient(Object.assign(Object.assign({}, _options), { additionalPolicies: [{ policy: imdsRetryPolicy(this.msiRetryConfig), position: "perCall" }] }));
this.managedIdentityApp = new ManagedIdentityApplication({
managedIdentityIdParams: {
userAssignedClientId: this.clientId,
userAssignedResourceId: this.resourceId,
userAssignedObjectId: this.objectId,
},
system: {
disableInternalRetries: true,
networkClient: this.identityClient,
loggerOptions: {
logLevel: getMSALLogLevel(getLogLevel()),
piiLoggingEnabled: (_b = _options.loggingOptions) === null || _b === void 0 ? void 0 : _b.enableUnsafeSupportLogging,
loggerCallback: defaultLoggerCallback(logger),
},
},
});
this.isAvailableIdentityClient = new IdentityClient(Object.assign(Object.assign({}, _options), { retryOptions: {
maxRetries: 0,
} }));
const managedIdentitySource = this.managedIdentityApp.getManagedIdentitySource();
// CloudShell MSI will ignore any user-assigned identity passed as parameters. To avoid confusion, we prevent this from happening as early as possible.
if (managedIdentitySource === "CloudShell") {
if (this.clientId || this.resourceId || this.objectId) {
logger.warning(`CloudShell MSI detected with user-provided IDs - throwing. Received values: ${JSON.stringify({
clientId: this.clientId,
resourceId: this.resourceId,
objectId: this.objectId,
})}.`);
throw new CredentialUnavailableError("ManagedIdentityCredential: Specifying a user-assigned managed identity is not supported for CloudShell at runtime. When using Managed Identity in CloudShell, omit the clientId, resourceId, and objectId parameters.");
}
}
// ServiceFabric does not support specifying user-assigned managed identity by client ID or resource ID. The managed identity selected is based on the resource configuration.
if (managedIdentitySource === "ServiceFabric") {
if (this.clientId || this.resourceId || this.objectId) {
logger.warning(`Service Fabric detected with user-provided IDs - throwing. Received values: ${JSON.stringify({
clientId: this.clientId,
resourceId: this.resourceId,
objectId: this.objectId,
})}.`);
throw new CredentialUnavailableError(`ManagedIdentityCredential: ${serviceFabricErrorMessage}`);
}
}
logger.info(`Using ${managedIdentitySource} managed identity.`);
// Check if either clientId, resourceId or objectId was provided and log the value used
if (providedIds.length === 1) {
const { key, value } = providedIds[0];
logger.info(`${managedIdentitySource} with ${key}: ${value}`);
}
}
/**
* Authenticates with Microsoft Entra ID and returns an access token if successful.
* If authentication fails, a {@link CredentialUnavailableError} will be thrown with the details of the failure.
* If an unexpected error occurs, an {@link AuthenticationError} will be thrown with the details of the failure.
*
* @param scopes - The list of scopes for which the token will have access.
* @param options - The options used to configure any requests this
* TokenCredential implementation might make.
*/
async getToken(scopes, options = {}) {
logger.getToken.info("Using the MSAL provider for Managed Identity.");
const resource = mapScopesToResource(scopes);
if (!resource) {
throw new CredentialUnavailableError(`ManagedIdentityCredential: Multiple scopes are not supported. Scopes: ${JSON.stringify(scopes)}`);
}
return tracingClient.withSpan("ManagedIdentityCredential.getToken", options, async () => {
var _a;
try {
const isTokenExchangeMsi = await tokenExchangeMsi.isAvailable(this.clientId);
// Most scenarios are handled by MSAL except for two:
// AKS pod identity - MSAL does not implement the token exchange flow.
// IMDS Endpoint probing - MSAL does not do any probing before trying to get a token.
// As a DefaultAzureCredential optimization we probe the IMDS endpoint with a short timeout and no retries before actually trying to get a token
// We will continue to implement these features in the Identity library.
const identitySource = this.managedIdentityApp.getManagedIdentitySource();
const isImdsMsi = identitySource === "DefaultToImds" || identitySource === "Imds"; // Neither actually checks that IMDS endpoint is available, just that it's the source the MSAL _would_ try to use.
logger.getToken.info(`MSAL Identity source: ${identitySource}`);
if (isTokenExchangeMsi) {
// In the AKS scenario we will use the existing tokenExchangeMsi indefinitely.
logger.getToken.info("Using the token exchange managed identity.");
const result = await tokenExchangeMsi.getToken({
scopes,
clientId: this.clientId,
identityClient: this.identityClient,
retryConfig: this.msiRetryConfig,
resourceId: this.resourceId,
});
if (result === null) {
throw new CredentialUnavailableError("Attempted to use the token exchange managed identity, but received a null response.");
}
return result;
}
else if (isImdsMsi) {
// In the IMDS scenario we will probe the IMDS endpoint to ensure it's available before trying to get a token.
// If the IMDS endpoint is not available and this is the source that MSAL will use, we will fail-fast with an error that tells DAC to move to the next credential.
logger.getToken.info("Using the IMDS endpoint to probe for availability.");
const isAvailable = await imdsMsi.isAvailable({
scopes,
clientId: this.clientId,
getTokenOptions: options,
identityClient: this.isAvailableIdentityClient,
resourceId: this.resourceId,
});
if (!isAvailable) {
throw new CredentialUnavailableError(`Attempted to use the IMDS endpoint, but it is not available.`);
}
}
// If we got this far, it means:
// - This is not a tokenExchangeMsi,
// - We already probed for IMDS endpoint availability and failed-fast if it's unreachable.
// We can proceed normally by calling MSAL for a token.
logger.getToken.info("Calling into MSAL for managed identity token.");
const token = await this.managedIdentityApp.acquireToken({
resource,
});
this.ensureValidMsalToken(scopes, token, options);
logger.getToken.info(formatSuccess(scopes));
return {
expiresOnTimestamp: token.expiresOn.getTime(),
token: token.accessToken,
refreshAfterTimestamp: (_a = token.refreshOn) === null || _a === void 0 ? void 0 : _a.getTime(),
tokenType: "Bearer",
};
}
catch (err) {
logger.getToken.error(formatError(scopes, err));
// AuthenticationRequiredError described as Error to enforce authentication after trying to retrieve a token silently.
// TODO: why would this _ever_ happen considering we're not trying the silent request in this flow?
if (err.name === "AuthenticationRequiredError") {
throw err;
}
if (isNetworkError(err)) {
throw new CredentialUnavailableError(`ManagedIdentityCredential: Network unreachable. Message: ${err.message}`, { cause: err });
}
throw new CredentialUnavailableError(`ManagedIdentityCredential: Authentication failed. Message ${err.message}`, { cause: err });
}
});
}
/**
* Ensures the validity of the MSAL token
*/
ensureValidMsalToken(scopes, msalToken, getTokenOptions) {
const createError = (message) => {
logger.getToken.info(message);
return new AuthenticationRequiredError({
scopes: Array.isArray(scopes) ? scopes : [scopes],
getTokenOptions,
message,
});
};
if (!msalToken) {
throw createError("No response.");
}
if (!msalToken.expiresOn) {
throw createError(`Response had no "expiresOn" property.`);
}
if (!msalToken.accessToken) {
throw createError(`Response had no "accessToken" property.`);
}
}
}
function isNetworkError(err) {
// MSAL error
if (err.errorCode === "network_error") {
return true;
}
// Probe errors
if (err.code === "ENETUNREACH" || err.code === "EHOSTUNREACH") {
return true;
}
// This is a special case for Docker Desktop which responds with a 403 with a message that contains "A socket operation was attempted to an unreachable network" or "A socket operation was attempted to an unreachable host"
// rather than just timing out, as expected.
if (err.statusCode === 403 || err.code === 403) {
if (err.message.includes("unreachable")) {
return true;
}
}
return false;
}
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
import { WorkloadIdentityCredential } from "../workloadIdentityCredential.js";
import { credentialLogger } from "../../util/logging.js";
const msiName = "ManagedIdentityCredential - Token Exchange";
const logger = credentialLogger(msiName);
/**
* Defines how to determine whether the token exchange MSI is available, and also how to retrieve a token from the token exchange MSI.
*
* Token exchange MSI (used by AKS) is the only MSI implementation handled entirely by Azure Identity.
* The rest have been migrated to MSAL.
*/
export const tokenExchangeMsi = {
name: "tokenExchangeMsi",
async isAvailable(clientId) {
const env = process.env;
const result = Boolean((clientId || env.AZURE_CLIENT_ID) &&
env.AZURE_TENANT_ID &&
process.env.AZURE_FEDERATED_TOKEN_FILE);
if (!result) {
logger.info(`${msiName}: Unavailable. The environment variables needed are: AZURE_CLIENT_ID (or the client ID sent through the parameters), AZURE_TENANT_ID and AZURE_FEDERATED_TOKEN_FILE`);
}
return result;
},
async getToken(configuration, getTokenOptions = {}) {
const { scopes, clientId } = configuration;
const identityClientTokenCredentialOptions = {};
const workloadIdentityCredential = new WorkloadIdentityCredential(Object.assign(Object.assign({ clientId, tenantId: process.env.AZURE_TENANT_ID, tokenFilePath: process.env.AZURE_FEDERATED_TOKEN_FILE }, identityClientTokenCredentialOptions), { disableInstanceDiscovery: true }));
return workloadIdentityCredential.getToken(scopes, getTokenOptions);
},
};
//# sourceMappingURL=tokenExchangeMsi.js.map

View File

@@ -0,0 +1,81 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
const DefaultScopeSuffix = "/.default";
/**
* Error message for Service Fabric Managed Identity environment.
*/
export const serviceFabricErrorMessage = "Specifying a `clientId` or `resourceId` is not supported by the Service Fabric managed identity environment. The managed identity configuration is determined by the Service Fabric cluster resource configuration. See https://aka.ms/servicefabricmi for more information";
/**
* Most MSIs send requests to the IMDS endpoint, or a similar endpoint.
* These are GET requests that require sending a `resource` parameter on the query.
* This resource can be derived from the scopes received through the getToken call, as long as only one scope is received.
* Multiple scopes assume that the resulting token will have access to multiple resources, which won't be the case.
*
* For that reason, when we encounter multiple scopes, we return undefined.
* It's up to the individual MSI implementations to throw the errors (which helps us provide less generic errors).
*/
export function mapScopesToResource(scopes) {
let scope = "";
if (Array.isArray(scopes)) {
if (scopes.length !== 1) {
return;
}
scope = scopes[0];
}
else if (typeof scopes === "string") {
scope = scopes;
}
if (!scope.endsWith(DefaultScopeSuffix)) {
return scope;
}
return scope.substr(0, scope.lastIndexOf(DefaultScopeSuffix));
}
/**
* Given a token response, return the expiration timestamp as the number of milliseconds from the Unix epoch.
* @param body - A parsed response body from the authentication endpoint.
*/
export function parseExpirationTimestamp(body) {
if (typeof body.expires_on === "number") {
return body.expires_on * 1000;
}
if (typeof body.expires_on === "string") {
const asNumber = +body.expires_on;
if (!isNaN(asNumber)) {
return asNumber * 1000;
}
const asDate = Date.parse(body.expires_on);
if (!isNaN(asDate)) {
return asDate;
}
}
if (typeof body.expires_in === "number") {
return Date.now() + body.expires_in * 1000;
}
throw new Error(`Failed to parse token expiration from body. expires_in="${body.expires_in}", expires_on="${body.expires_on}"`);
}
/**
* Given a token response, return the expiration timestamp as the number of milliseconds from the Unix epoch.
* @param body - A parsed response body from the authentication endpoint.
*/
export function parseRefreshTimestamp(body) {
if (body.refresh_on) {
if (typeof body.refresh_on === "number") {
return body.refresh_on * 1000;
}
if (typeof body.refresh_on === "string") {
const asNumber = +body.refresh_on;
if (!isNaN(asNumber)) {
return asNumber * 1000;
}
const asDate = Date.parse(body.refresh_on);
if (!isNaN(asDate)) {
return asDate;
}
}
throw new Error(`Failed to parse refresh_on from body. refresh_on="${body.refresh_on}"`);
}
else {
return undefined;
}
}
//# sourceMappingURL=utils.js.map