@azure/identity
Version:
Provides credential implementations for Azure SDK libraries that can authenticate with Microsoft Entra ID
253 lines • 13.5 kB
JavaScript
"use strict";
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
Object.defineProperty(exports, "__esModule", { value: true });
exports.ManagedIdentityCredential = void 0;
const logger_1 = require("@azure/logger");
const msal_node_1 = require("@azure/msal-node");
const identityClient_js_1 = require("../../client/identityClient.js");
const errors_js_1 = require("../../errors.js");
const utils_js_1 = require("../../msal/utils.js");
const imdsRetryPolicy_js_1 = require("./imdsRetryPolicy.js");
const logging_js_1 = require("../../util/logging.js");
const tracing_js_1 = require("../../util/tracing.js");
const imdsMsi_js_1 = require("./imdsMsi.js");
const tokenExchangeMsi_js_1 = require("./tokenExchangeMsi.js");
const utils_js_2 = require("./utils.js");
const logger = (0, logging_js_1.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
*/
class ManagedIdentityCredential {
managedIdentityApp;
identityClient;
clientId;
resourceId;
objectId;
msiRetryConfig = {
maxRetries: 5,
startDelayInMs: 800,
intervalIncrement: 2,
};
isAvailableIdentityClient;
/**
* @internal
* @hidden
*/
constructor(clientIdOrOptions, options) {
let _options;
if (typeof clientIdOrOptions === "string") {
this.clientId = clientIdOrOptions;
_options = options ?? {};
}
else {
this.clientId = clientIdOrOptions?.clientId;
_options = clientIdOrOptions ?? {};
}
this.resourceId = _options?.resourceId;
this.objectId = _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 (_options.retryOptions?.maxRetries !== undefined) {
this.msiRetryConfig.maxRetries = _options.retryOptions.maxRetries;
}
this.identityClient = new identityClient_js_1.IdentityClient({
..._options,
additionalPolicies: [{ policy: (0, imdsRetryPolicy_js_1.imdsRetryPolicy)(this.msiRetryConfig), position: "perCall" }],
});
this.managedIdentityApp = new msal_node_1.ManagedIdentityApplication({
managedIdentityIdParams: {
userAssignedClientId: this.clientId,
userAssignedResourceId: this.resourceId,
userAssignedObjectId: this.objectId,
},
system: {
disableInternalRetries: true,
networkClient: this.identityClient,
loggerOptions: {
logLevel: (0, utils_js_1.getMSALLogLevel)((0, logger_1.getLogLevel)()),
piiLoggingEnabled: _options.loggingOptions?.enableUnsafeSupportLogging,
loggerCallback: (0, utils_js_1.defaultLoggerCallback)(logger),
},
},
});
this.isAvailableIdentityClient = new identityClient_js_1.IdentityClient({
..._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 errors_js_1.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 errors_js_1.CredentialUnavailableError(`ManagedIdentityCredential: ${utils_js_2.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 = (0, utils_js_2.mapScopesToResource)(scopes);
if (!resource) {
throw new errors_js_1.CredentialUnavailableError(`ManagedIdentityCredential: Multiple scopes are not supported. Scopes: ${JSON.stringify(scopes)}`);
}
return tracing_js_1.tracingClient.withSpan("ManagedIdentityCredential.getToken", options, async () => {
try {
const isTokenExchangeMsi = await tokenExchangeMsi_js_1.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_js_1.tokenExchangeMsi.getToken({
scopes,
clientId: this.clientId,
identityClient: this.identityClient,
retryConfig: this.msiRetryConfig,
resourceId: this.resourceId,
});
if (result === null) {
throw new errors_js_1.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_js_1.imdsMsi.isAvailable({
scopes,
clientId: this.clientId,
getTokenOptions: options,
identityClient: this.isAvailableIdentityClient,
resourceId: this.resourceId,
});
if (!isAvailable) {
throw new errors_js_1.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((0, logging_js_1.formatSuccess)(scopes));
return {
expiresOnTimestamp: token.expiresOn.getTime(),
token: token.accessToken,
refreshAfterTimestamp: token.refreshOn?.getTime(),
tokenType: "Bearer",
};
}
catch (err) {
logger.getToken.error((0, logging_js_1.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 errors_js_1.CredentialUnavailableError(`ManagedIdentityCredential: Network unreachable. Message: ${err.message}`, { cause: err });
}
throw new errors_js_1.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 errors_js_1.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.`);
}
}
}
exports.ManagedIdentityCredential = ManagedIdentityCredential;
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