diff --git a/sdk/identity/identity/src/credentials/managedIdentityCredential/appServiceMsi2017.ts b/sdk/identity/identity/src/credentials/managedIdentityCredential/appServiceMsi2017.ts deleted file mode 100644 index fe0d1b1e4756..000000000000 --- a/sdk/identity/identity/src/credentials/managedIdentityCredential/appServiceMsi2017.ts +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import type { PipelineRequestOptions } from "@azure/core-rest-pipeline"; -import { createHttpHeaders, createPipelineRequest } from "@azure/core-rest-pipeline"; -import type { GetTokenOptions } from "@azure/core-auth"; -import { credentialLogger } from "../../util/logging"; -import type { MSI, MSIConfiguration, MSIToken } from "./models"; -import { mapScopesToResource } from "./utils"; - -const msiName = "ManagedIdentityCredential - AppServiceMSI 2017"; -const logger = credentialLogger(msiName); - -/** - * Generates the options used on the request for an access token. - */ -function prepareRequestOptions( - scopes: string | string[], - clientId?: string, -): PipelineRequestOptions { - const resource = mapScopesToResource(scopes); - if (!resource) { - throw new Error(`${msiName}: Multiple scopes are not supported.`); - } - - const queryParameters: Record = { - resource, - "api-version": "2017-09-01", - }; - - if (clientId) { - queryParameters.clientid = clientId; - } - - const query = new URLSearchParams(queryParameters); - - // This error should not bubble up, since we verify that this environment variable is defined in the isAvailable() method defined below. - if (!process.env.MSI_ENDPOINT) { - throw new Error(`${msiName}: Missing environment variable: MSI_ENDPOINT`); - } - if (!process.env.MSI_SECRET) { - throw new Error(`${msiName}: Missing environment variable: MSI_SECRET`); - } - - return { - url: `${process.env.MSI_ENDPOINT}?${query.toString()}`, - method: "GET", - headers: createHttpHeaders({ - Accept: "application/json", - secret: process.env.MSI_SECRET, - }), - }; -} - -/** - * Defines how to determine whether the Azure App Service MSI is available, and also how to retrieve a token from the Azure App Service MSI. - */ -export const appServiceMsi2017: MSI = { - name: "appServiceMsi2017", - async isAvailable({ scopes }): Promise { - const resource = mapScopesToResource(scopes); - if (!resource) { - logger.info(`${msiName}: Unavailable. Multiple scopes are not supported.`); - return false; - } - const env = process.env; - const result = Boolean(env.MSI_ENDPOINT && env.MSI_SECRET); - if (!result) { - logger.info( - `${msiName}: Unavailable. The environment variables needed are: MSI_ENDPOINT and MSI_SECRET.`, - ); - } - return result; - }, - async getToken( - configuration: MSIConfiguration, - getTokenOptions: GetTokenOptions = {}, - ): Promise { - const { identityClient, scopes, clientId, resourceId } = configuration; - - if (resourceId) { - logger.warning( - `${msiName}: managed Identity by resource Id is not supported. Argument resourceId might be ignored by the service.`, - ); - } - - logger.info( - `${msiName}: Using the endpoint and the secret coming form the environment variables: MSI_ENDPOINT=${process.env.MSI_ENDPOINT} and MSI_SECRET=[REDACTED].`, - ); - - const request = createPipelineRequest({ - abortSignal: getTokenOptions.abortSignal, - ...prepareRequestOptions(scopes, clientId), - // Generally, MSI endpoints use the HTTP protocol, without transport layer security (TLS). - allowInsecureConnection: true, - }); - const tokenResponse = await identityClient.sendTokenRequest(request); - return (tokenResponse && tokenResponse.accessToken) || null; - }, -}; diff --git a/sdk/identity/identity/src/credentials/managedIdentityCredential/appServiceMsi2019.ts b/sdk/identity/identity/src/credentials/managedIdentityCredential/appServiceMsi2019.ts deleted file mode 100644 index 0af03a8e7cab..000000000000 --- a/sdk/identity/identity/src/credentials/managedIdentityCredential/appServiceMsi2019.ts +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import type { PipelineRequestOptions } from "@azure/core-rest-pipeline"; -import { createHttpHeaders, createPipelineRequest } from "@azure/core-rest-pipeline"; -import type { GetTokenOptions } from "@azure/core-auth"; -import { credentialLogger } from "../../util/logging"; -import type { MSI, MSIConfiguration, MSIToken } from "./models"; -import { mapScopesToResource } from "./utils"; - -const msiName = "ManagedIdentityCredential - AppServiceMSI 2019"; -const logger = credentialLogger(msiName); - -/** - * Generates the options used on the request for an access token. - */ -function prepareRequestOptions( - scopes: string | string[], - clientId?: string, - resourceId?: string, -): PipelineRequestOptions { - const resource = mapScopesToResource(scopes); - if (!resource) { - throw new Error(`${msiName}: Multiple scopes are not supported.`); - } - - const queryParameters: Record = { - resource, - "api-version": "2019-08-01", - }; - - if (clientId) { - queryParameters.client_id = clientId; - } - - if (resourceId) { - queryParameters.mi_res_id = resourceId; - } - const query = new URLSearchParams(queryParameters); - - // This error should not bubble up, since we verify that this environment variable is defined in the isAvailable() method defined below. - if (!process.env.IDENTITY_ENDPOINT) { - throw new Error(`${msiName}: Missing environment variable: IDENTITY_ENDPOINT`); - } - if (!process.env.IDENTITY_HEADER) { - throw new Error(`${msiName}: Missing environment variable: IDENTITY_HEADER`); - } - - return { - url: `${process.env.IDENTITY_ENDPOINT}?${query.toString()}`, - method: "GET", - headers: createHttpHeaders({ - Accept: "application/json", - "X-IDENTITY-HEADER": process.env.IDENTITY_HEADER, - }), - }; -} - -/** - * Defines how to determine whether the Azure App Service MSI is available, and also how to retrieve a token from the Azure App Service MSI. - */ -export const appServiceMsi2019: MSI = { - name: "appServiceMsi2019", - async isAvailable({ scopes }): Promise { - const resource = mapScopesToResource(scopes); - if (!resource) { - logger.info(`${msiName}: Unavailable. Multiple scopes are not supported.`); - return false; - } - const env = process.env; - const result = Boolean(env.IDENTITY_ENDPOINT && env.IDENTITY_HEADER); - if (!result) { - logger.info( - `${msiName}: Unavailable. The environment variables needed are: IDENTITY_ENDPOINT and IDENTITY_HEADER.`, - ); - } - return result; - }, - async getToken( - configuration: MSIConfiguration, - getTokenOptions: GetTokenOptions = {}, - ): Promise { - const { identityClient, scopes, clientId, resourceId } = configuration; - - logger.info( - `${msiName}: Using the endpoint and the secret coming form the environment variables: IDENTITY_ENDPOINT=${process.env.IDENTITY_ENDPOINT} and IDENTITY_HEADER=[REDACTED].`, - ); - - const request = createPipelineRequest({ - abortSignal: getTokenOptions.abortSignal, - ...prepareRequestOptions(scopes, clientId, resourceId), - // Generally, MSI endpoints use the HTTP protocol, without transport layer security (TLS). - allowInsecureConnection: true, - }); - const tokenResponse = await identityClient.sendTokenRequest(request); - return (tokenResponse && tokenResponse.accessToken) || null; - }, -}; diff --git a/sdk/identity/identity/src/credentials/managedIdentityCredential/arcMsi.ts b/sdk/identity/identity/src/credentials/managedIdentityCredential/arcMsi.ts deleted file mode 100644 index b6c3ea3331ff..000000000000 --- a/sdk/identity/identity/src/credentials/managedIdentityCredential/arcMsi.ts +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import type { MSI, MSIConfiguration, MSIToken } from "./models"; -import type { PipelineRequestOptions } from "@azure/core-rest-pipeline"; -import { createHttpHeaders, createPipelineRequest } from "@azure/core-rest-pipeline"; - -import { AuthenticationError } from "../../errors"; -import type { GetTokenOptions } from "@azure/core-auth"; -import type { IdentityClient } from "../../client/identityClient"; -import { azureArcAPIVersion } from "./constants"; -import { credentialLogger } from "../../util/logging"; -import fs from "node:fs"; -import { mapScopesToResource } from "./utils"; - -const msiName = "ManagedIdentityCredential - Azure Arc MSI"; -const logger = credentialLogger(msiName); - -/** - * Generates the options used on the request for an access token. - */ -function prepareRequestOptions( - scopes: string | string[], - clientId?: string, - resourceId?: string, -): PipelineRequestOptions { - const resource = mapScopesToResource(scopes); - if (!resource) { - throw new Error(`${msiName}: Multiple scopes are not supported.`); - } - const queryParameters: Record = { - resource, - "api-version": azureArcAPIVersion, - }; - - if (clientId) { - queryParameters.client_id = clientId; - } - if (resourceId) { - queryParameters.msi_res_id = resourceId; - } - - // This error should not bubble up, since we verify that this environment variable is defined in the isAvailable() method defined below. - if (!process.env.IDENTITY_ENDPOINT) { - throw new Error(`${msiName}: Missing environment variable: IDENTITY_ENDPOINT`); - } - - const query = new URLSearchParams(queryParameters); - - return createPipelineRequest({ - // Should be similar to: http://localhost:40342/metadata/identity/oauth2/token - url: `${process.env.IDENTITY_ENDPOINT}?${query.toString()}`, - method: "GET", - headers: createHttpHeaders({ - Accept: "application/json", - Metadata: "true", - }), - }); -} - -/** - * Does a request to the authentication provider that results in a file path. - */ -async function filePathRequest( - identityClient: IdentityClient, - requestPrepareOptions: PipelineRequestOptions, -): Promise { - const response = await identityClient.sendRequest(createPipelineRequest(requestPrepareOptions)); - - if (response.status !== 401) { - let message = ""; - if (response.bodyAsText) { - message = ` Response: ${response.bodyAsText}`; - } - throw new AuthenticationError( - response.status, - `${msiName}: To authenticate with Azure Arc MSI, status code 401 is expected on the first request. ${message}`, - ); - } - - const authHeader = response.headers.get("www-authenticate") || ""; - try { - return authHeader.split("=").slice(1)[0]; - } catch (e: any) { - throw Error(`Invalid www-authenticate header format: ${authHeader}`); - } -} - -export function platformToFilePath(): string { - switch (process.platform) { - case "win32": - if (!process.env.PROGRAMDATA) { - throw new Error(`${msiName}: PROGRAMDATA environment variable has no value.`); - } - return `${process.env.PROGRAMDATA}\\AzureConnectedMachineAgent\\Tokens`; - case "linux": - return "/var/opt/azcmagent/tokens"; - default: - throw new Error(`${msiName}: Unsupported platform ${process.platform}.`); - } -} - -/** - * Validates that a given Azure Arc MSI file path is valid for use. - * - * A valid file will: - * 1. Be in the expected path for the current platform. - * 2. Have a `.key` extension. - * 3. Be at most 4096 bytes in size. - */ -export function validateKeyFile(filePath?: string): asserts filePath is string { - if (!filePath) { - throw new Error(`${msiName}: Failed to find the token file.`); - } - - if (!filePath.endsWith(".key")) { - throw new Error(`${msiName}: unexpected file path from HIMDS service: ${filePath}.`); - } - - const expectedPath = platformToFilePath(); - if (!filePath.startsWith(expectedPath)) { - throw new Error(`${msiName}: unexpected file path from HIMDS service: ${filePath}.`); - } - - const stats = fs.statSync(filePath); - if (stats.size > 4096) { - throw new Error( - `${msiName}: The file at ${filePath} is larger than expected at ${stats.size} bytes.`, - ); - } -} - -/** - * Defines how to determine whether the Azure Arc MSI is available, and also how to retrieve a token from the Azure Arc MSI. - */ -export const arcMsi: MSI = { - name: "arc", - async isAvailable({ scopes }): Promise { - const resource = mapScopesToResource(scopes); - if (!resource) { - logger.info(`${msiName}: Unavailable. Multiple scopes are not supported.`); - return false; - } - const result = Boolean(process.env.IMDS_ENDPOINT && process.env.IDENTITY_ENDPOINT); - if (!result) { - logger.info( - `${msiName}: The environment variables needed are: IMDS_ENDPOINT and IDENTITY_ENDPOINT`, - ); - } - return result; - }, - async getToken( - configuration: MSIConfiguration, - getTokenOptions: GetTokenOptions = {}, - ): Promise { - const { identityClient, scopes, clientId, resourceId } = configuration; - - if (clientId) { - logger.warning( - `${msiName}: user-assigned identities not supported. The argument clientId might be ignored by the service.`, - ); - } - if (resourceId) { - logger.warning( - `${msiName}: user defined managed Identity by resource Id is not supported. Argument resourceId will be ignored.`, - ); - } - - logger.info(`${msiName}: Authenticating.`); - - const requestOptions = { - disableJsonStringifyOnBody: true, - deserializationMapper: undefined, - abortSignal: getTokenOptions.abortSignal, - ...prepareRequestOptions(scopes, clientId, resourceId), - allowInsecureConnection: true, - }; - - const filePath = await filePathRequest(identityClient, requestOptions); - validateKeyFile(filePath); - - const key = await fs.promises.readFile(filePath, { encoding: "utf-8" }); - requestOptions.headers?.set("Authorization", `Basic ${key}`); - - const request = createPipelineRequest({ - ...requestOptions, - // Generally, MSI endpoints use the HTTP protocol, without transport layer security (TLS). - allowInsecureConnection: true, - }); - const tokenResponse = await identityClient.sendTokenRequest(request); - return (tokenResponse && tokenResponse.accessToken) || null; - }, -}; diff --git a/sdk/identity/identity/src/credentials/managedIdentityCredential/cloudShellMsi.ts b/sdk/identity/identity/src/credentials/managedIdentityCredential/cloudShellMsi.ts deleted file mode 100644 index bbb84ca9a929..000000000000 --- a/sdk/identity/identity/src/credentials/managedIdentityCredential/cloudShellMsi.ts +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import type { PipelineRequestOptions } from "@azure/core-rest-pipeline"; -import { createHttpHeaders, createPipelineRequest } from "@azure/core-rest-pipeline"; -import { credentialLogger } from "../../util/logging"; -import type { GetTokenOptions } from "@azure/core-auth"; -import type { MSI, MSIConfiguration, MSIToken } from "./models"; -import { mapScopesToResource } from "./utils"; - -const msiName = "ManagedIdentityCredential - CloudShellMSI"; -export const logger = credentialLogger(msiName); - -/** - * Generates the options used on the request for an access token. - */ -function prepareRequestOptions( - scopes: string | string[], - clientId?: string, - resourceId?: string, -): PipelineRequestOptions { - const resource = mapScopesToResource(scopes); - if (!resource) { - throw new Error(`${msiName}: Multiple scopes are not supported.`); - } - - const body: Record = { - resource, - }; - - if (clientId) { - body.client_id = clientId; - } - if (resourceId) { - body.msi_res_id = resourceId; - } - - // This error should not bubble up, since we verify that this environment variable is defined in the isAvailable() method defined below. - if (!process.env.MSI_ENDPOINT) { - throw new Error(`${msiName}: Missing environment variable: MSI_ENDPOINT`); - } - const params = new URLSearchParams(body); - return { - url: process.env.MSI_ENDPOINT, - method: "POST", - body: params.toString(), - headers: createHttpHeaders({ - Accept: "application/json", - Metadata: "true", - "Content-Type": "application/x-www-form-urlencoded", - }), - }; -} - -/** - * Defines how to determine whether the Azure Cloud Shell MSI is available, and also how to retrieve a token from the Azure Cloud Shell MSI. - * Since Azure Managed Identities aren't available in the Azure Cloud Shell, we log a warning for users that try to access cloud shell using user assigned identity. - */ -export const cloudShellMsi: MSI = { - name: "cloudShellMsi", - async isAvailable({ scopes }): Promise { - const resource = mapScopesToResource(scopes); - if (!resource) { - logger.info(`${msiName}: Unavailable. Multiple scopes are not supported.`); - return false; - } - - const result = Boolean(process.env.MSI_ENDPOINT); - if (!result) { - logger.info(`${msiName}: Unavailable. The environment variable MSI_ENDPOINT is needed.`); - } - return result; - }, - async getToken( - configuration: MSIConfiguration, - getTokenOptions: GetTokenOptions = {}, - ): Promise { - const { identityClient, scopes, clientId, resourceId } = configuration; - - if (clientId) { - logger.warning( - `${msiName}: user-assigned identities not supported. The argument clientId might be ignored by the service.`, - ); - } - - if (resourceId) { - logger.warning( - `${msiName}: user defined managed Identity by resource Id not supported. The argument resourceId might be ignored by the service.`, - ); - } - - logger.info( - `${msiName}: Using the endpoint coming form the environment variable MSI_ENDPOINT = ${process.env.MSI_ENDPOINT}.`, - ); - - const request = createPipelineRequest({ - abortSignal: getTokenOptions.abortSignal, - ...prepareRequestOptions(scopes, clientId, resourceId), - // Generally, MSI endpoints use the HTTP protocol, without transport layer security (TLS). - allowInsecureConnection: true, - }); - const tokenResponse = await identityClient.sendTokenRequest(request); - return (tokenResponse && tokenResponse.accessToken) || null; - }, -}; diff --git a/sdk/identity/identity/src/credentials/managedIdentityCredential/constants.ts b/sdk/identity/identity/src/credentials/managedIdentityCredential/constants.ts deleted file mode 100644 index 89e5dda446ae..000000000000 --- a/sdk/identity/identity/src/credentials/managedIdentityCredential/constants.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -export const DefaultScopeSuffix = "/.default"; -export const imdsHost = "http://169.254.169.254"; -export const imdsEndpointPath = "/metadata/identity/oauth2/token"; -export const imdsApiVersion = "2018-02-01"; -export const azureArcAPIVersion = "2019-11-01"; -export const azureFabricVersion = "2019-07-01-preview"; diff --git a/sdk/identity/identity/src/credentials/managedIdentityCredential/fabricMsi.ts b/sdk/identity/identity/src/credentials/managedIdentityCredential/fabricMsi.ts deleted file mode 100644 index 10903274ee0c..000000000000 --- a/sdk/identity/identity/src/credentials/managedIdentityCredential/fabricMsi.ts +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import https from "https"; -import type { PipelineRequestOptions } from "@azure/core-rest-pipeline"; -import { createHttpHeaders, createPipelineRequest } from "@azure/core-rest-pipeline"; -import type { GetTokenOptions } from "@azure/core-auth"; -import { credentialLogger } from "../../util/logging"; -import type { MSI, MSIConfiguration, MSIToken } from "./models"; -import { mapScopesToResource } from "./utils"; -import { azureFabricVersion } from "./constants"; - -// This MSI can be easily tested by deploying a container to Azure Service Fabric with the Dockerfile: -// -// FROM node:12 -// RUN wget https://host.any/path/bash.sh -// CMD ["bash", "bash.sh"] -// -// Where the bash script contains: -// -// curl --insecure $IDENTITY_ENDPOINT'?api-version=2019-07-01-preview&resource=https://vault.azure.net/' -H "Secret: $IDENTITY_HEADER" -// - -const msiName = "ManagedIdentityCredential - Fabric MSI"; -const logger = credentialLogger(msiName); - -/** - * Generates the options used on the request for an access token. - */ -function prepareRequestOptions( - scopes: string | string[], - clientId?: string, - resourceId?: string, -): PipelineRequestOptions { - const resource = mapScopesToResource(scopes); - if (!resource) { - throw new Error(`${msiName}: Multiple scopes are not supported.`); - } - - const queryParameters: Record = { - resource, - "api-version": azureFabricVersion, - }; - - if (clientId) { - queryParameters.client_id = clientId; - } - if (resourceId) { - queryParameters.msi_res_id = resourceId; - } - const query = new URLSearchParams(queryParameters); - - // This error should not bubble up, since we verify that this environment variable is defined in the isAvailable() method defined below. - if (!process.env.IDENTITY_ENDPOINT) { - throw new Error("Missing environment variable: IDENTITY_ENDPOINT"); - } - if (!process.env.IDENTITY_HEADER) { - throw new Error("Missing environment variable: IDENTITY_HEADER"); - } - - return { - url: `${process.env.IDENTITY_ENDPOINT}?${query.toString()}`, - method: "GET", - headers: createHttpHeaders({ - Accept: "application/json", - secret: process.env.IDENTITY_HEADER, - }), - }; -} - -/** - * Defines how to determine whether the Azure Service Fabric MSI is available, and also how to retrieve a token from the Azure Service Fabric MSI. - */ -export const fabricMsi: MSI = { - name: "fabricMsi", - async isAvailable({ scopes }): Promise { - const resource = mapScopesToResource(scopes); - if (!resource) { - logger.info(`${msiName}: Unavailable. Multiple scopes are not supported.`); - return false; - } - const env = process.env; - const result = Boolean( - env.IDENTITY_ENDPOINT && env.IDENTITY_HEADER && env.IDENTITY_SERVER_THUMBPRINT, - ); - if (!result) { - logger.info( - `${msiName}: Unavailable. The environment variables needed are: IDENTITY_ENDPOINT, IDENTITY_HEADER and IDENTITY_SERVER_THUMBPRINT`, - ); - } - return result; - }, - async getToken( - configuration: MSIConfiguration, - getTokenOptions: GetTokenOptions = {}, - ): Promise { - const { scopes, identityClient, clientId, resourceId } = configuration; - - if (resourceId) { - logger.warning( - `${msiName}: user defined managed Identity by resource Id is not supported. Argument resourceId might be ignored by the service.`, - ); - } - - logger.info( - [ - `${msiName}:`, - "Using the endpoint and the secret coming from the environment variables:", - `IDENTITY_ENDPOINT=${process.env.IDENTITY_ENDPOINT},`, - "IDENTITY_HEADER=[REDACTED] and", - "IDENTITY_SERVER_THUMBPRINT=[REDACTED].", - ].join(" "), - ); - - const request = createPipelineRequest({ - abortSignal: getTokenOptions.abortSignal, - ...prepareRequestOptions(scopes, clientId, resourceId), - // The service fabric MSI endpoint will be HTTPS (however, the certificate will be self-signed). - // allowInsecureConnection: true - }); - - request.agent = new https.Agent({ - // This is necessary because Service Fabric provides a self-signed certificate. - // The alternative path is to verify the certificate using the IDENTITY_SERVER_THUMBPRINT env variable. - rejectUnauthorized: false, - }); - - const tokenResponse = await identityClient.sendTokenRequest(request); - return (tokenResponse && tokenResponse.accessToken) || null; - }, -}; diff --git a/sdk/identity/identity/src/credentials/managedIdentityCredential/imdsMsi.ts b/sdk/identity/identity/src/credentials/managedIdentityCredential/imdsMsi.ts index f00807bc4d54..84af318ac883 100644 --- a/sdk/identity/identity/src/credentials/managedIdentityCredential/imdsMsi.ts +++ b/sdk/identity/identity/src/credentials/managedIdentityCredential/imdsMsi.ts @@ -1,21 +1,23 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import type { MSI, MSIConfiguration, MSIToken } from "./models"; import type { PipelineRequestOptions, PipelineResponse } from "@azure/core-rest-pipeline"; import { createHttpHeaders, createPipelineRequest } from "@azure/core-rest-pipeline"; -import { delay, isError } from "@azure/core-util"; -import { imdsApiVersion, imdsEndpointPath, imdsHost } from "./constants"; +import { isError } from "@azure/core-util"; -import { AuthenticationError } from "../../errors"; import type { GetTokenOptions } from "@azure/core-auth"; import { credentialLogger } from "../../util/logging"; import { mapScopesToResource } from "./utils"; import { tracingClient } from "../../util/tracing"; +import { IdentityClient } from "../../client/identityClient"; const msiName = "ManagedIdentityCredential - IMDS"; const logger = credentialLogger(msiName); +export const imdsHost = "http://169.254.169.254"; +export const imdsEndpointPath = "/metadata/identity/oauth2/token"; +export const imdsApiVersion = "2018-02-01"; + /** * Generates the options used on the request for an access token. */ @@ -74,17 +76,20 @@ function prepareRequestOptions( } /** - * Defines how to determine whether the Azure IMDS MSI is available, and also how to retrieve a token from the Azure IMDS MSI. + * 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: MSI = { +export const imdsMsi = { name: "imdsMsi", - async isAvailable({ - scopes, - identityClient, - clientId, - resourceId, - getTokenOptions = {}, + async isAvailable(options: { + scopes: string | string[]; + identityClient?: IdentityClient; + clientId?: string; + resourceId?: string; + getTokenOptions?: GetTokenOptions; }): Promise { + const { scopes, identityClient, clientId, resourceId, getTokenOptions } = options; const resource = mapScopesToResource(scopes); if (!resource) { logger.info(`${msiName}: Unavailable. Multiple scopes are not supported.`); @@ -107,9 +112,9 @@ export const imdsMsi: MSI = { return tracingClient.withSpan( "ManagedIdentityCredential-pingImdsEndpoint", - getTokenOptions, - async (options) => { - requestOptions.tracingOptions = options.tracingOptions; + getTokenOptions ?? {}, + async (updatedOptions) => { + 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 @@ -118,7 +123,7 @@ export const imdsMsi: MSI = { // Default to 1000 if the default of 0 is used. // Negative values can still be used to disable the timeout. - request.timeout = options.requestOptions?.timeout || 1000; + request.timeout = updatedOptions.requestOptions?.timeout || 1000; // This MSI uses the imdsEndpoint to get the token, which only uses http:// request.allowInsecureConnection = true; @@ -150,44 +155,4 @@ export const imdsMsi: MSI = { }, ); }, - async getToken( - configuration: MSIConfiguration, - getTokenOptions: GetTokenOptions = {}, - ): Promise { - const { identityClient, scopes, clientId, resourceId } = configuration; - - if (process.env.AZURE_POD_IDENTITY_AUTHORITY_HOST) { - logger.info( - `${msiName}: Using the Azure IMDS endpoint coming from the environment variable AZURE_POD_IDENTITY_AUTHORITY_HOST=${process.env.AZURE_POD_IDENTITY_AUTHORITY_HOST}.`, - ); - } else { - logger.info(`${msiName}: Using the default Azure IMDS endpoint ${imdsHost}.`); - } - - let nextDelayInMs = configuration.retryConfig.startDelayInMs; - for (let retries = 0; retries < configuration.retryConfig.maxRetries; retries++) { - try { - const request = createPipelineRequest({ - abortSignal: getTokenOptions.abortSignal, - ...prepareRequestOptions(scopes, clientId, resourceId), - allowInsecureConnection: true, - }); - const tokenResponse = await identityClient.sendTokenRequest(request); - - return (tokenResponse && tokenResponse.accessToken) || null; - } catch (error: any) { - if (error.statusCode === 404) { - await delay(nextDelayInMs); - nextDelayInMs *= configuration.retryConfig.intervalIncrement; - continue; - } - throw error; - } - } - - throw new AuthenticationError( - 404, - `${msiName}: Failed to retrieve IMDS token after ${configuration.retryConfig.maxRetries} retries.`, - ); - }, }; diff --git a/sdk/identity/identity/src/credentials/managedIdentityCredential/index.browser.ts b/sdk/identity/identity/src/credentials/managedIdentityCredential/index.browser.ts index 6b3202c628ab..a0fed57619fd 100644 --- a/sdk/identity/identity/src/credentials/managedIdentityCredential/index.browser.ts +++ b/sdk/identity/identity/src/credentials/managedIdentityCredential/index.browser.ts @@ -3,7 +3,6 @@ import type { AccessToken, TokenCredential } from "@azure/core-auth"; -import type { TokenCredentialOptions } from "../../tokenCredentialOptions"; import { credentialLogger, formatError } from "../../util/logging"; const BrowserNotSupportedError = new Error( @@ -12,8 +11,6 @@ const BrowserNotSupportedError = new Error( const logger = credentialLogger("ManagedIdentityCredential"); export class ManagedIdentityCredential implements TokenCredential { - constructor(clientId: string, options?: TokenCredentialOptions); - constructor(options?: TokenCredentialOptions); constructor() { logger.info(formatError("", BrowserNotSupportedError)); throw BrowserNotSupportedError; diff --git a/sdk/identity/identity/src/credentials/managedIdentityCredential/index.ts b/sdk/identity/identity/src/credentials/managedIdentityCredential/index.ts index 241ad1d17f36..a065ec741674 100644 --- a/sdk/identity/identity/src/credentials/managedIdentityCredential/index.ts +++ b/sdk/identity/identity/src/credentials/managedIdentityCredential/index.ts @@ -9,16 +9,17 @@ import { ManagedIdentityApplication } from "@azure/msal-node"; import { IdentityClient } from "../../client/identityClient"; import { AuthenticationRequiredError, CredentialUnavailableError } from "../../errors"; import { getMSALLogLevel, defaultLoggerCallback } from "../../msal/utils"; -import { logger } from "./cloudShellMsi"; import { imdsRetryPolicy } from "./imdsRetryPolicy"; import { MSIConfiguration } from "./models"; -import { formatSuccess, formatError } from "../../util/logging"; +import { formatSuccess, formatError, credentialLogger } from "../../util/logging"; import { tracingClient } from "../../util/tracing"; import { imdsMsi } from "./imdsMsi"; import { tokenExchangeMsi } from "./tokenExchangeMsi"; import { mapScopesToResource } from "./utils"; import { MsalToken, ValidMsalToken } from "../../msal/types"; +const logger = credentialLogger("ManagedIdentityCredential"); + /** * Options to send on the {@link ManagedIdentityCredential} constructor. * This variation supports `clientId` and not `resourceId`, since only one of both is supported. @@ -116,10 +117,10 @@ export class ManagedIdentityCredential implements TokenCredential { | ManagedIdentityCredentialObjectIdOptions, options?: TokenCredentialOptions, ) { - let _options: TokenCredentialOptions | undefined; + let _options: TokenCredentialOptions; if (typeof clientIdOrOptions === "string") { this.clientId = clientIdOrOptions; - _options = options; + _options = options ?? {}; } else { this.clientId = (clientIdOrOptions as ManagedIdentityCredentialClientIdOptions)?.clientId; _options = clientIdOrOptions ?? {}; @@ -138,7 +139,6 @@ export class ManagedIdentityCredential implements TokenCredential { } // ManagedIdentity uses http for local requests - _options ??= {}; _options.allowInsecureConnection = true; if (_options.retryOptions?.maxRetries !== undefined) { @@ -157,7 +157,6 @@ export class ManagedIdentityCredential implements TokenCredential { userAssignedObjectId: this.objectId, }, system: { - // todo: proxyUrl? disableInternalRetries: true, networkClient: this.identityClient, loggerOptions: { @@ -219,13 +218,7 @@ export class ManagedIdentityCredential implements TokenCredential { return tracingClient.withSpan("ManagedIdentityCredential.getToken", options, async () => { try { - const isTokenExchangeMsi = await tokenExchangeMsi.isAvailable({ - scopes, - clientId: this.clientId, - getTokenOptions: options, - identityClient: this.identityClient, - resourceId: this.resourceId, - }); + 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. diff --git a/sdk/identity/identity/src/credentials/managedIdentityCredential/models.ts b/sdk/identity/identity/src/credentials/managedIdentityCredential/models.ts index 175fbe2a0d94..453e60ee0e93 100644 --- a/sdk/identity/identity/src/credentials/managedIdentityCredential/models.ts +++ b/sdk/identity/identity/src/credentials/managedIdentityCredential/models.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import type { AccessToken, GetTokenOptions } from "@azure/core-auth"; +import type { AccessToken } from "@azure/core-auth"; import type { IdentityClient } from "../../client/identityClient"; @@ -26,21 +26,3 @@ export interface MSIConfiguration { * with an expiration time and the time in which token should refresh. */ export declare interface MSIToken extends AccessToken {} - -/** - * @internal - */ -export interface MSI { - name: string; - isAvailable(options: { - scopes: string | string[]; - identityClient?: IdentityClient; - clientId?: string; - resourceId?: string; - getTokenOptions?: GetTokenOptions; - }): Promise; - getToken( - configuration: MSIConfiguration, - getTokenOptions?: GetTokenOptions, - ): Promise; -} diff --git a/sdk/identity/identity/src/credentials/managedIdentityCredential/tokenExchangeMsi.ts b/sdk/identity/identity/src/credentials/managedIdentityCredential/tokenExchangeMsi.ts index 60311e7c362a..997c0bb5fcde 100644 --- a/sdk/identity/identity/src/credentials/managedIdentityCredential/tokenExchangeMsi.ts +++ b/sdk/identity/identity/src/credentials/managedIdentityCredential/tokenExchangeMsi.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import type { AccessToken, GetTokenOptions } from "@azure/core-auth"; -import type { MSI, MSIConfiguration } from "./models"; +import type { MSIConfiguration } from "./models"; import { WorkloadIdentityCredential } from "../workloadIdentityCredential"; import { credentialLogger } from "../../util/logging"; import type { WorkloadIdentityCredentialOptions } from "../workloadIdentityCredentialOptions"; @@ -12,10 +12,13 @@ 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: MSI = { +export const tokenExchangeMsi = { name: "tokenExchangeMsi", - async isAvailable({ clientId }): Promise { + async isAvailable(clientId?: string): Promise { const env = process.env; const result = Boolean( (clientId || env.AZURE_CLIENT_ID) && diff --git a/sdk/identity/identity/src/credentials/managedIdentityCredential/utils.ts b/sdk/identity/identity/src/credentials/managedIdentityCredential/utils.ts index 579bb1c92d52..f303281c4d14 100644 --- a/sdk/identity/identity/src/credentials/managedIdentityCredential/utils.ts +++ b/sdk/identity/identity/src/credentials/managedIdentityCredential/utils.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { DefaultScopeSuffix } from "./constants"; +const DefaultScopeSuffix = "/.default"; /** * Most MSIs send requests to the IMDS endpoint, or a similar endpoint. diff --git a/sdk/identity/identity/test/internal/node/managedIdentityCredential/arcMsi.spec.ts b/sdk/identity/identity/test/internal/node/managedIdentityCredential/arcMsi.spec.ts deleted file mode 100644 index 1a8c045c4343..000000000000 --- a/sdk/identity/identity/test/internal/node/managedIdentityCredential/arcMsi.spec.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { - platformToFilePath, - validateKeyFile, -} from "../../../../src/credentials/managedIdentityCredential/arcMsi"; - -import type { Context } from "mocha"; -import Sinon from "sinon"; -import { assert } from "chai"; -import fs from "node:fs"; -import path from "node:path"; - -describe("arcMsi", function () { - afterEach(function () { - Sinon.restore(); - }); - - describe("validateKeyFile", function () { - let expectedDirectory: string; - - beforeEach(function () { - if (process.platform !== "win32" && process.platform !== "linux") { - // Not supported on this platform - this.skip(); - } - expectedDirectory = platformToFilePath(); - }); - - it("succeeds if the file is valid", function (this: Context) { - const filePath = path.join(expectedDirectory, "file.key"); - Sinon.stub(fs, "statSync").returns({ size: 4096 } as any); - assert.doesNotThrow(() => validateKeyFile(filePath)); - }); - - it("throws if file path is empty", function () { - assert.throws(() => validateKeyFile(""), /Failed to find/); - assert.throws(() => validateKeyFile(undefined), /Failed to find/); - }); - - describe("on Windows", function () { - it("throws when the file is not in the expected path", function () { - Sinon.stub(process, "platform").value("win32"); - Sinon.stub(process, "env").get(() => { - return { - PROGRAMDATA: "C:\\ProgramData", - }; - }); - assert.throws(() => validateKeyFile("C:\\Users\\user\\file.key"), /unexpected file path/); - }); - - it("throws if ProgramData is undefined", function () { - Sinon.stub(process, "platform").value("win32"); - Sinon.stub(process, "env").get(() => { - return { - PROGRAMDATA: undefined, - }; - }); - assert.throws( - () => validateKeyFile("C:\\Users\\user\\file.key"), - /PROGRAMDATA environment variable/, - ); - }); - }); - - describe("on Linux", function () { - it("throws when the file is not in the expected path", function () { - Sinon.stub(process, "platform").value("linux"); - assert.throws(() => validateKeyFile("/home/user/file.key"), /unexpected file path/); - }); - }); - - it("throws if the file extension is not .key", function () { - const filePath = path.join(expectedDirectory, "file.pem"); - assert.throws(() => validateKeyFile(filePath), /unexpected file path/); - }); - - it("throws if the file size is invalid", function () { - const filePath = path.join(expectedDirectory, "file.key"); - Sinon.stub(fs, "statSync").returns({ size: 4097 } as any); - assert.throws(() => validateKeyFile(filePath), /larger than expected/); - }); - }); -});