UNPKG

@azure/identity

Version:

Provides credential implementations for Azure SDK libraries that can authenticate with Microsoft Entra ID

309 lines (308 loc) • 14.7 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var workloadIdentityCredential_exports = {}; __export(workloadIdentityCredential_exports, { SupportedWorkloadEnvironmentVariables: () => SupportedWorkloadEnvironmentVariables, WorkloadIdentityCredential: () => WorkloadIdentityCredential, parseAndValidateCustomTokenProxy: () => parseAndValidateCustomTokenProxy }); module.exports = __toCommonJS(workloadIdentityCredential_exports); var import_logging = require("../util/logging.js"); var import_clientAssertionCredential = require("./clientAssertionCredential.js"); var import_errors = require("../errors.js"); var import_tenantIdUtils = require("../util/tenantIdUtils.js"); var import_promises = require("node:fs/promises"); var import_core_rest_pipeline = require("@azure/core-rest-pipeline"); var import_certificatesUtils = require("../util/certificatesUtils.js"); var import_node_fs = require("node:fs"); const credentialName = "WorkloadIdentityCredential"; const SupportedWorkloadEnvironmentVariables = [ "AZURE_TENANT_ID", "AZURE_CLIENT_ID", "AZURE_FEDERATED_TOKEN_FILE" ]; const logger = (0, import_logging.credentialLogger)(credentialName); const ErrorMessages = { FAILED_TO_PARSE_TOKEN_PROXY: (endpoint, error) => `Failed to parse custom token proxy URL "${endpoint}": ${error}`, INVALID_HTTPS_SCHEME: (protocol) => `Custom token endpoint must use https scheme, got "${protocol}"`, TOKEN_ENDPOINT_NO_USER_INFO: (url) => `Custom token endpoint URL "${url}" must not contain user info`, TOKEN_ENDPOINT_NO_QUERY: (url) => `Custom token endpoint URL "${url}" must not contain a query`, TOKEN_ENDPOINT_NO_FRAGMENT: (url) => `Custom token endpoint URL "${url}" must not contain a fragment`, CA_FILE_EMPTY: (file) => `CA certificate file is empty: ${file}`, FAILED_TO_READ_CA_FILE: (file, error) => `Failed to read CA certificate file: ${file}. ${error}`, INVALID_CA_CERTIFICATES: `Invalid CA certificate data: no valid PEM certificates found`, INVALID_FILE_PATH: (path) => `Invalid file path provided ${path}.`, NO_FILE_CONTENT: (path) => `No content on the file ${path}.`, NO_CA_SOURCE: `No CA certificate source specified.`, CLIENT_ID_REQUIRED: `clientId is a required parameter. In DefaultAzureCredential and ManagedIdentityCredential, this can be provided as an environment variable - "AZURE_CLIENT_ID". See the troubleshooting guide for more information: https://aka.ms/azsdk/js/identity/workloadidentitycredential/troubleshoot`, TENANT_ID_REQUIRED: `tenantId is a required parameter. In DefaultAzureCredential and ManagedIdentityCredential, this can be provided as an environment variable - "AZURE_TENANT_ID". See the troubleshooting guide for more information: https://aka.ms/azsdk/js/identity/workloadidentitycredential/troubleshoot`, TOKEN_FILE_PATH_REQUIRED: `federatedTokenFilePath is a required parameter. In DefaultAzureCredential and ManagedIdentityCredential, this can be provided as an environment variable - "AZURE_FEDERATED_TOKEN_FILE". See the troubleshooting guide for more information: https://aka.ms/azsdk/js/identity/workloadidentitycredential/troubleshoot`, TOKEN_PROXY_NOT_SET: `AZURE_KUBERNETES_TOKEN_PROXY is not set but other custom endpoint-related environment variables are present`, CA_FILE_AND_DATA_EXCLUSIVE: `AZURE_KUBERNETES_CA_FILE and AZURE_KUBERNETES_CA_DATA are mutually exclusive. Specify only one.`, MISSING_ENV_VARS: `tenantId, clientId, and federatedTokenFilePath are required parameters. In DefaultAzureCredential and ManagedIdentityCredential, these can be provided as environment variables - "AZURE_TENANT_ID", "AZURE_CLIENT_ID", "AZURE_FEDERATED_TOKEN_FILE". See the troubleshooting guide for more information: https://aka.ms/azsdk/js/identity/workloadidentitycredential/troubleshoot` }; function parseAndValidateCustomTokenProxy(endpoint) { let tokenProxy; try { tokenProxy = new URL(endpoint); } catch (error) { throw new import_errors.CredentialUnavailableError( `${credentialName}: is unavailable. ${ErrorMessages.FAILED_TO_PARSE_TOKEN_PROXY(endpoint, error)}` ); } if (tokenProxy.protocol !== "https:") { throw new import_errors.CredentialUnavailableError( `${credentialName}: is unavailable. ${ErrorMessages.INVALID_HTTPS_SCHEME(tokenProxy.protocol)}` ); } if (tokenProxy.username || tokenProxy.password) { throw new import_errors.CredentialUnavailableError( `${credentialName}: is unavailable. ${ErrorMessages.TOKEN_ENDPOINT_NO_USER_INFO(tokenProxy.toString())}` ); } if (tokenProxy.search) { throw new import_errors.CredentialUnavailableError( `${credentialName}: is unavailable. ${ErrorMessages.TOKEN_ENDPOINT_NO_QUERY(tokenProxy.toString())}` ); } if (tokenProxy.hash) { throw new import_errors.CredentialUnavailableError( `${credentialName}: is unavailable. ${ErrorMessages.TOKEN_ENDPOINT_NO_FRAGMENT(tokenProxy.toString())}` ); } if (!tokenProxy.pathname || tokenProxy.pathname === "") { tokenProxy.pathname = "/"; } return tokenProxy.toString(); } class WorkloadIdentityCredential { client; azureFederatedTokenFileContent = void 0; cacheDate = void 0; federatedTokenFilePath; // AKS proxy CA caching - persists across token requests cachedTlsSettings; cachedCaData; caData; caFile; sniName; /** * WorkloadIdentityCredential supports Microsoft Entra Workload ID on Kubernetes. * * @param options - The identity client options to use for authentication. */ constructor(options) { const assignedEnv = (0, import_logging.processEnvVars)(SupportedWorkloadEnvironmentVariables).assigned.join(", "); logger.info(`Found the following environment variables: ${assignedEnv}`); const workloadIdentityCredentialOptions = options ?? {}; const tenantId = workloadIdentityCredentialOptions.tenantId || process.env.AZURE_TENANT_ID; const clientId = workloadIdentityCredentialOptions.clientId || process.env.AZURE_CLIENT_ID; this.federatedTokenFilePath = workloadIdentityCredentialOptions.tokenFilePath || process.env.AZURE_FEDERATED_TOKEN_FILE; if (tenantId) { (0, import_tenantIdUtils.checkTenantId)(logger, tenantId); } if (!clientId) { throw new import_errors.CredentialUnavailableError( `${credentialName}: is unavailable. ${ErrorMessages.CLIENT_ID_REQUIRED}` ); } if (!tenantId) { throw new import_errors.CredentialUnavailableError( `${credentialName}: is unavailable. ${ErrorMessages.TENANT_ID_REQUIRED}` ); } if (!this.federatedTokenFilePath) { throw new import_errors.CredentialUnavailableError( `${credentialName}: is unavailable. ${ErrorMessages.TOKEN_FILE_PATH_REQUIRED}` ); } if (workloadIdentityCredentialOptions.enableAzureProxy) { const kubernetesTokenProxy = process.env.AZURE_KUBERNETES_TOKEN_PROXY; const kubernetesSNIName = process.env.AZURE_KUBERNETES_SNI_NAME; const kubernetesCAFile = process.env.AZURE_KUBERNETES_CA_FILE; const kubernetesCAData = process.env.AZURE_KUBERNETES_CA_DATA; if (!kubernetesTokenProxy) { if (kubernetesSNIName || kubernetesCAFile || kubernetesCAData) { throw new import_errors.CredentialUnavailableError( `${credentialName}: is unavailable. ${ErrorMessages.TOKEN_PROXY_NOT_SET}` ); } logger.info( `enableAzureProxy is true but AZURE_KUBERNETES_TOKEN_PROXY is not set, using normal authentication flow` ); } else { const tokenProxy = parseAndValidateCustomTokenProxy(kubernetesTokenProxy); if (kubernetesCAFile && kubernetesCAData) { throw new import_errors.CredentialUnavailableError( `${credentialName}: is unavailable. ${ErrorMessages.CA_FILE_AND_DATA_EXCLUSIVE}` ); } this.caData = kubernetesCAData; this.caFile = kubernetesCAFile; this.sniName = kubernetesSNIName; const proxyClient = this.createAksProxyClient(tokenProxy); workloadIdentityCredentialOptions.httpClient = proxyClient; logger.info(`${credentialName}: Using proxy client for token requests`); } } logger.info( `Invoking ClientAssertionCredential with tenant ID: ${tenantId}, clientId: ${workloadIdentityCredentialOptions.clientId} and federated token path: [REDACTED]` ); this.client = new import_clientAssertionCredential.ClientAssertionCredential( tenantId, clientId, this.readFileContents.bind(this), workloadIdentityCredentialOptions ); } /** * Creates a proxy HttpClient that intercepts token requests and redirects them to the Kubernetes endpoint * Caching is handled at the credential level to persist across token requests */ createAksProxyClient(tokenEndpoint) { const defaultClient = (0, import_core_rest_pipeline.createDefaultHttpClient)(); this.cachedTlsSettings = this.getTlsSettings(); return { sendRequest: async (request) => { const requestUrl = new URL(request.url); logger.info( `${credentialName}: Redirecting request to Kubernetes endpoint: ${tokenEndpoint}` ); const proxyUrl = new URL(tokenEndpoint); const requestPath = requestUrl.pathname.replace(/^\//, ""); const combinedPath = proxyUrl.pathname.endsWith("/") ? proxyUrl.pathname + requestPath : proxyUrl.pathname + "/" + requestPath; const newUrl = new URL(proxyUrl.origin); newUrl.pathname = combinedPath; newUrl.search = requestUrl.search; newUrl.hash = requestUrl.hash; request.url = newUrl.toString(); request.tlsSettings = this.getTlsSettings(); logger.info(`${credentialName}: Sending request to ${request.url}`); return defaultClient.sendRequest(request); } }; } /** * Gets TLS settings for the request. * Handles a few scenarios with CA data or CA file provided. */ getTlsSettings() { if (!this.caData && !this.caFile) { if (!this.cachedTlsSettings) { this.cachedTlsSettings = this.sniName ? { servername: this.sniName } : {}; } return this.cachedTlsSettings; } if (!this.caFile) { if (!this.cachedTlsSettings) { if (!(0, import_certificatesUtils.canParseAsX509Certificate)(this.caData)) { throw new import_errors.CredentialUnavailableError( `${credentialName}: is unavailable. ${ErrorMessages.INVALID_CA_CERTIFICATES}` ); } this.cachedTlsSettings = this.sniName ? { servername: this.sniName } : {}; this.cachedTlsSettings.ca = this.caData; } return this.cachedTlsSettings; } let fileContent; try { fileContent = (0, import_node_fs.readFileSync)(this.caFile); } catch (error) { throw new import_errors.CredentialUnavailableError( `${credentialName}: is unavailable. ${ErrorMessages.FAILED_TO_READ_CA_FILE(this.caFile, error)}` ); } if (fileContent.length === 0) { if (!this.cachedTlsSettings) { throw new import_errors.CredentialUnavailableError( `${credentialName}: is unavailable. ${ErrorMessages.CA_FILE_EMPTY(this.caFile)}` ); } return this.cachedTlsSettings; } if (!this.cachedCaData || !fileContent.equals(this.cachedCaData)) { const caDataString = fileContent.toString("utf8"); if (!(0, import_certificatesUtils.canParseAsX509Certificate)(caDataString)) { throw new import_errors.CredentialUnavailableError( `${credentialName}: is unavailable. ${ErrorMessages.INVALID_CA_CERTIFICATES}` ); } this.cachedTlsSettings = { ca: caDataString, ...this.sniName && { servername: this.sniName } }; this.cachedCaData = fileContent; } return this.cachedTlsSettings; } /** * 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. * * @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) { if (!this.client) { const errorMessage = `${credentialName}: is unavailable. ${ErrorMessages.MISSING_ENV_VARS}`; logger.info(errorMessage); throw new import_errors.CredentialUnavailableError(errorMessage); } logger.info("Invoking getToken() of Client Assertion Credential"); return this.client.getToken(scopes, options); } async readFileContents() { if (this.cacheDate !== void 0 && Date.now() - this.cacheDate >= 1e3 * 60 * 5) { this.azureFederatedTokenFileContent = void 0; } if (!this.federatedTokenFilePath) { throw new import_errors.CredentialUnavailableError( `${credentialName}: is unavailable. ${ErrorMessages.INVALID_FILE_PATH(this.federatedTokenFilePath)}` ); } if (!this.azureFederatedTokenFileContent) { const file = await (0, import_promises.readFile)(this.federatedTokenFilePath, "utf8"); const value = file.trim(); if (!value) { throw new import_errors.CredentialUnavailableError( `${credentialName}: is unavailable. ${ErrorMessages.NO_FILE_CONTENT(this.federatedTokenFilePath)}` ); } else { this.azureFederatedTokenFileContent = value; this.cacheDate = Date.now(); } } return this.azureFederatedTokenFileContent; } } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { SupportedWorkloadEnvironmentVariables, WorkloadIdentityCredential, parseAndValidateCustomTokenProxy }); //# sourceMappingURL=workloadIdentityCredential.js.map