From 4221c74e6b9441a4b84ab1af12e085b5142cfc7a Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Mon, 4 Nov 2024 16:45:11 +0000 Subject: [PATCH] builds --- .../legacyMsiProvider.ts | 440 ---- .../msalMsiProvider.ts | 314 --- .../node/azureApplicationCredential.spec.ts | 5 +- .../internal/node/legacyMsiProvider.spec.ts | 2126 ++++++++--------- .../msalMsiProvider.spec.ts | 48 +- 5 files changed, 1090 insertions(+), 1843 deletions(-) delete mode 100644 sdk/identity/identity/src/credentials/managedIdentityCredential/legacyMsiProvider.ts delete mode 100644 sdk/identity/identity/src/credentials/managedIdentityCredential/msalMsiProvider.ts diff --git a/sdk/identity/identity/src/credentials/managedIdentityCredential/legacyMsiProvider.ts b/sdk/identity/identity/src/credentials/managedIdentityCredential/legacyMsiProvider.ts deleted file mode 100644 index 9a80e63cfdcb..000000000000 --- a/sdk/identity/identity/src/credentials/managedIdentityCredential/legacyMsiProvider.ts +++ /dev/null @@ -1,440 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import type { AccessToken, GetTokenOptions } from "@azure/core-auth"; -import type { AppTokenProviderParameters } from "@azure/msal-node"; -import { ConfidentialClientApplication } from "@azure/msal-node"; -import { - AuthenticationError, - AuthenticationRequiredError, - CredentialUnavailableError, -} from "../../errors"; -import type { MSI, MSIConfiguration, MSIToken } from "./models"; -import type { MsalResult, MsalToken, ValidMsalToken } from "../../msal/types"; -import { cloudShellMsi } from "./cloudShellMsi"; -import { credentialLogger, formatError, formatSuccess } from "../../util/logging"; - -import { DeveloperSignOnClientId } from "../../constants"; -import { IdentityClient } from "../../client/identityClient"; -import type { TokenCredentialOptions } from "../../tokenCredentialOptions"; -import { appServiceMsi2017 } from "./appServiceMsi2017"; -import { appServiceMsi2019 } from "./appServiceMsi2019"; -import { arcMsi } from "./arcMsi"; -import { fabricMsi } from "./fabricMsi"; -import { getLogLevel } from "@azure/logger"; -import { getMSALLogLevel } from "../../msal/utils"; -import { imdsMsi } from "./imdsMsi"; -import { tokenExchangeMsi } from "./tokenExchangeMsi"; -import { tracingClient } from "../../util/tracing"; - -const logger = credentialLogger("ManagedIdentityCredential"); - -// As part of the migration of Managed Identity to MSAL, this legacy provider captures the existing behavior -// ported over from the ManagedIdentityCredential verbatim. This is to ensure that the existing behavior -// is maintained while the new implementation is being tested and validated. -// https://github.com/Azure/azure-sdk-for-js/issues/30189 tracks deleting this provider once it is no longer needed. - -/** - * Options to send on the {@link ManagedIdentityCredential} constructor. - * Since this is an internal implementation, uses a looser interface than the public one. - */ -interface ManagedIdentityCredentialOptions extends TokenCredentialOptions { - /** - * The client ID of the user - assigned identity, or app registration(when working with AKS pod - identity). - */ - clientId?: string; - - /** - * Allows specifying a custom resource Id. - * In scenarios such as when user assigned identities are created using an ARM template, - * where the resource Id of the identity is known but the client Id can't be known ahead of time, - * this parameter allows programs to use these user assigned identities - * without having to first determine the client Id of the created identity. - */ - resourceId?: string; -} - -export class LegacyMsiProvider { - private identityClient: IdentityClient; - private clientId: string | undefined; - private resourceId: string | undefined; - private isEndpointUnavailable: boolean | null = null; - private isAvailableIdentityClient: IdentityClient; - private confidentialApp: ConfidentialClientApplication; - private isAppTokenProviderInitialized: boolean = false; - private msiRetryConfig: MSIConfiguration["retryConfig"] = { - maxRetries: 5, - startDelayInMs: 800, - intervalIncrement: 2, - }; - - constructor( - clientIdOrOptions?: string | ManagedIdentityCredentialOptions, - options?: TokenCredentialOptions, - ) { - let _options: TokenCredentialOptions | undefined; - if (typeof clientIdOrOptions === "string") { - this.clientId = clientIdOrOptions; - _options = options; - } else { - this.clientId = (clientIdOrOptions as ManagedIdentityCredentialOptions)?.clientId; - _options = clientIdOrOptions; - } - this.resourceId = (_options as ManagedIdentityCredentialOptions)?.resourceId; - // For JavaScript users. - if (this.clientId && this.resourceId) { - throw new Error( - `ManagedIdentityCredential - Client Id and Resource Id can't be provided at the same time.`, - ); - } - if (_options?.retryOptions?.maxRetries !== undefined) { - this.msiRetryConfig.maxRetries = _options.retryOptions.maxRetries; - } - this.identityClient = new IdentityClient(_options); - this.isAvailableIdentityClient = new IdentityClient({ - ..._options, - retryOptions: { - maxRetries: 0, - }, - }); - - /** authority host validation and metadata discovery to be skipped in managed identity - * since this wasn't done previously before adding token cache support - */ - this.confidentialApp = new ConfidentialClientApplication({ - auth: { - authority: "https://login.microsoftonline.com/managed_identity", - clientId: this.clientId ?? DeveloperSignOnClientId, - clientSecret: "dummy-secret", - cloudDiscoveryMetadata: - '{"tenant_discovery_endpoint":"https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration","api-version":"1.1","metadata":[{"preferred_network":"login.microsoftonline.com","preferred_cache":"login.windows.net","aliases":["login.microsoftonline.com","login.windows.net","login.microsoft.com","sts.windows.net"]},{"preferred_network":"login.partner.microsoftonline.cn","preferred_cache":"login.partner.microsoftonline.cn","aliases":["login.partner.microsoftonline.cn","login.chinacloudapi.cn"]},{"preferred_network":"login.microsoftonline.de","preferred_cache":"login.microsoftonline.de","aliases":["login.microsoftonline.de"]},{"preferred_network":"login.microsoftonline.us","preferred_cache":"login.microsoftonline.us","aliases":["login.microsoftonline.us","login.usgovcloudapi.net"]},{"preferred_network":"login-us.microsoftonline.com","preferred_cache":"login-us.microsoftonline.com","aliases":["login-us.microsoftonline.com"]}]}', - authorityMetadata: - '{"token_endpoint":"https://login.microsoftonline.com/common/oauth2/v2.0/token","token_endpoint_auth_methods_supported":["client_secret_post","private_key_jwt","client_secret_basic"],"jwks_uri":"https://login.microsoftonline.com/common/discovery/v2.0/keys","response_modes_supported":["query","fragment","form_post"],"subject_types_supported":["pairwise"],"id_token_signing_alg_values_supported":["RS256"],"response_types_supported":["code","id_token","code id_token","id_token token"],"scopes_supported":["openid","profile","email","offline_access"],"issuer":"https://login.microsoftonline.com/{tenantid}/v2.0","request_uri_parameter_supported":false,"userinfo_endpoint":"https://graph.microsoft.com/oidc/userinfo","authorization_endpoint":"https://login.microsoftonline.com/common/oauth2/v2.0/authorize","device_authorization_endpoint":"https://login.microsoftonline.com/common/oauth2/v2.0/devicecode","http_logout_supported":true,"frontchannel_logout_supported":true,"end_session_endpoint":"https://login.microsoftonline.com/common/oauth2/v2.0/logout","claims_supported":["sub","iss","cloud_instance_name","cloud_instance_host_name","cloud_graph_host_name","msgraph_host","aud","exp","iat","auth_time","acr","nonce","preferred_username","name","tid","ver","at_hash","c_hash","email"],"kerberos_endpoint":"https://login.microsoftonline.com/common/kerberos","tenant_region_scope":null,"cloud_instance_name":"microsoftonline.com","cloud_graph_host_name":"graph.windows.net","msgraph_host":"graph.microsoft.com","rbac_url":"https://pas.windows.net"}', - clientCapabilities: [], - }, - system: { - loggerOptions: { - logLevel: getMSALLogLevel(getLogLevel()), - }, - }, - }); - } - - private cachedMSI: MSI | undefined; - - private async cachedAvailableMSI( - scopes: string | string[], - getTokenOptions?: GetTokenOptions, - ): Promise { - if (this.cachedMSI) { - return this.cachedMSI; - } - - const MSIs = [ - arcMsi, - fabricMsi, - appServiceMsi2019, - appServiceMsi2017, - cloudShellMsi, - tokenExchangeMsi, - imdsMsi, - ]; - - for (const msi of MSIs) { - if ( - await msi.isAvailable({ - scopes, - identityClient: this.isAvailableIdentityClient, - clientId: this.clientId, - resourceId: this.resourceId, - getTokenOptions, - }) - ) { - this.cachedMSI = msi; - return msi; - } - } - - throw new CredentialUnavailableError(`ManagedIdentityCredential - No MSI credential available`); - } - - private async authenticateManagedIdentity( - scopes: string | string[], - getTokenOptions?: GetTokenOptions, - ): Promise { - const { span, updatedOptions } = tracingClient.startSpan( - `ManagedIdentityCredential.authenticateManagedIdentity`, - getTokenOptions, - ); - - try { - // Determining the available MSI, and avoiding checking for other MSIs while the program is running. - const availableMSI = await this.cachedAvailableMSI(scopes, updatedOptions); - return availableMSI.getToken( - { - identityClient: this.identityClient, - scopes, - clientId: this.clientId, - resourceId: this.resourceId, - retryConfig: this.msiRetryConfig, - }, - updatedOptions, - ); - } catch (err: any) { - span.setStatus({ - status: "error", - error: err, - }); - throw err; - } finally { - span.end(); - } - } - - /** - * 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. - */ - public async getToken( - scopes: string | string[], - options?: GetTokenOptions, - ): Promise { - let result: AccessToken | null = null; - const { span, updatedOptions } = tracingClient.startSpan( - `ManagedIdentityCredential.getToken`, - options, - ); - try { - // isEndpointAvailable can be true, false, or null, - // If it's null, it means we don't yet know whether - // the endpoint is available and need to check for it. - if (this.isEndpointUnavailable !== true) { - const availableMSI = await this.cachedAvailableMSI(scopes, updatedOptions); - if (availableMSI.name === "tokenExchangeMsi") { - result = await this.authenticateManagedIdentity(scopes, updatedOptions); - } else { - const appTokenParameters: AppTokenProviderParameters = { - correlationId: this.identityClient.getCorrelationId(), - tenantId: options?.tenantId || "managed_identity", - scopes: Array.isArray(scopes) ? scopes : [scopes], - claims: options?.claims, - }; - - // Added a check to see if SetAppTokenProvider was already defined. - this.initializeSetAppTokenProvider(); - const authenticationResult = await this.confidentialApp.acquireTokenByClientCredential({ - ...appTokenParameters, - }); - result = this.handleResult(scopes, authenticationResult || undefined); - } - if (result === null) { - // If authenticateManagedIdentity returns null, - // it means no MSI endpoints are available. - // If so, we avoid trying to reach to them in future requests. - this.isEndpointUnavailable = true; - - // It also means that the endpoint answered with either 200 or 201 (see the sendTokenRequest method), - // yet we had no access token. For this reason, we'll throw once with a specific message: - const error = new CredentialUnavailableError( - "The managed identity endpoint was reached, yet no tokens were received.", - ); - logger.getToken.info(formatError(scopes, error)); - throw error; - } - - // Since `authenticateManagedIdentity` didn't throw, and the result was not null, - // We will assume that this endpoint is reachable from this point forward, - // and avoid pinging again to it. - this.isEndpointUnavailable = false; - } else { - // We've previously determined that the endpoint was unavailable, - // either because it was unreachable or permanently unable to authenticate. - const error = new CredentialUnavailableError( - "The managed identity endpoint is not currently available", - ); - logger.getToken.info(formatError(scopes, error)); - throw error; - } - - logger.getToken.info(formatSuccess(scopes)); - return result; - } catch (err: any) { - // CredentialUnavailable errors are expected to reach here. - // We intend them to bubble up, so that DefaultAzureCredential can catch them. - if (err.name === "AuthenticationRequiredError") { - throw err; - } - - // Expected errors to reach this point: - // - Errors coming from a method unexpectedly breaking. - // - When identityClient.sendTokenRequest throws, in which case - // if the status code was 400, it means that the endpoint is working, - // but no identity is available. - - span.setStatus({ - status: "error", - error: err, - }); - - // If either the network is unreachable, - // we can safely assume the credential is unavailable. - if (err.code === "ENETUNREACH") { - const error = new CredentialUnavailableError( - `ManagedIdentityCredential: Unavailable. Network unreachable. Message: ${err.message}`, - ); - - logger.getToken.info(formatError(scopes, error)); - throw error; - } - - // If either the host was unreachable, - // we can safely assume the credential is unavailable. - if (err.code === "EHOSTUNREACH") { - const error = new CredentialUnavailableError( - `ManagedIdentityCredential: Unavailable. No managed identity endpoint found. Message: ${err.message}`, - ); - - logger.getToken.info(formatError(scopes, error)); - throw error; - } - // If err.statusCode has a value of 400, it comes from sendTokenRequest, - // and it means that the endpoint is working, but that no identity is available. - if (err.statusCode === 400) { - throw new CredentialUnavailableError( - `ManagedIdentityCredential: The managed identity endpoint is indicating there's no available identity. Message: ${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. - if (err.statusCode === 403 || err.code === 403) { - if (err.message.includes("unreachable")) { - const error = new CredentialUnavailableError( - `ManagedIdentityCredential: Unavailable. Network unreachable. Message: ${err.message}`, - ); - - logger.getToken.info(formatError(scopes, error)); - throw error; - } - } - - // If the error has no status code, we can assume there was no available identity. - // This will throw silently during any ChainedTokenCredential. - if (err.statusCode === undefined) { - throw new CredentialUnavailableError( - `ManagedIdentityCredential: Authentication failed. Message ${err.message}`, - ); - } - - // Any other error should break the chain. - throw new AuthenticationError(err.statusCode, { - error: `ManagedIdentityCredential authentication failed.`, - error_description: err.message, - }); - } finally { - // Finally is always called, both if we return and if we throw in the above try/catch. - span.end(); - } - } - - /** - * Handles the MSAL authentication result. - * If the result has an account, we update the local account reference. - * If the token received is invalid, an error will be thrown depending on what's missing. - */ - private handleResult( - scopes: string | string[], - result?: MsalResult, - getTokenOptions?: GetTokenOptions, - ): AccessToken { - this.ensureValidMsalToken(scopes, result, getTokenOptions); - logger.getToken.info(formatSuccess(scopes)); - return { - token: result.accessToken, - expiresOnTimestamp: result.expiresOn.getTime(), - refreshAfterTimestamp: result.refreshOn?.getTime(), - tokenType: "Bearer", - } as AccessToken; - } - - /** - * Ensures the validity of the MSAL token - */ - private ensureValidMsalToken( - scopes: string | string[], - msalToken?: MsalToken, - getTokenOptions?: GetTokenOptions, - ): asserts msalToken is ValidMsalToken { - const error = (message: string): Error => { - logger.getToken.info(message); - return new AuthenticationRequiredError({ - scopes: Array.isArray(scopes) ? scopes : [scopes], - getTokenOptions, - message, - }); - }; - if (!msalToken) { - throw error("No response"); - } - if (!msalToken.expiresOn) { - throw error(`Response had no "expiresOn" property.`); - } - if (!msalToken.accessToken) { - throw error(`Response had no "accessToken" property.`); - } - } - - private initializeSetAppTokenProvider(): void { - if (!this.isAppTokenProviderInitialized) { - this.confidentialApp.SetAppTokenProvider(async (appTokenProviderParameters) => { - logger.info( - `SetAppTokenProvider invoked with parameters- ${JSON.stringify( - appTokenProviderParameters, - )}`, - ); - const getTokenOptions: GetTokenOptions = { - ...appTokenProviderParameters, - }; - logger.info( - `authenticateManagedIdentity invoked with scopes- ${JSON.stringify( - appTokenProviderParameters.scopes, - )} and getTokenOptions - ${JSON.stringify(getTokenOptions)}`, - ); - const resultToken = await this.authenticateManagedIdentity( - appTokenProviderParameters.scopes, - getTokenOptions, - ); - - if (resultToken) { - logger.info(`SetAppTokenProvider will save the token in cache`); - - const expiresInSeconds = resultToken?.expiresOnTimestamp - ? Math.floor((resultToken.expiresOnTimestamp - Date.now()) / 1000) - : 0; - const refreshInSeconds = resultToken?.refreshAfterTimestamp - ? Math.floor((resultToken.refreshAfterTimestamp - Date.now()) / 1000) - : 0; - return { - accessToken: resultToken?.token, - expiresInSeconds, - refreshInSeconds, - }; - } else { - logger.info( - `SetAppTokenProvider token has "no_access_token_returned" as the saved token`, - ); - return { - accessToken: "no_access_token_returned", - expiresInSeconds: 0, - }; - } - }); - this.isAppTokenProviderInitialized = true; - } - } -} diff --git a/sdk/identity/identity/src/credentials/managedIdentityCredential/msalMsiProvider.ts b/sdk/identity/identity/src/credentials/managedIdentityCredential/msalMsiProvider.ts deleted file mode 100644 index b0819f36b8a9..000000000000 --- a/sdk/identity/identity/src/credentials/managedIdentityCredential/msalMsiProvider.ts +++ /dev/null @@ -1,314 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import type { AccessToken, GetTokenOptions } from "@azure/core-auth"; -import { AuthenticationRequiredError, CredentialUnavailableError } from "../../errors"; -import type { MsalToken, ValidMsalToken } from "../../msal/types"; -import { credentialLogger, formatError, formatSuccess } from "../../util/logging"; -import { defaultLoggerCallback, getMSALLogLevel } from "../../msal/utils"; - -import { IdentityClient } from "../../client/identityClient"; -import type { MSIConfiguration } from "./models"; -import { ManagedIdentityApplication } from "@azure/msal-node"; -import type { TokenCredentialOptions } from "../../tokenCredentialOptions"; -import { getLogLevel } from "@azure/logger"; -import { imdsMsi } from "./imdsMsi"; -import { imdsRetryPolicy } from "./imdsRetryPolicy"; -import { mapScopesToResource } from "./utils"; -import { tokenExchangeMsi } from "./tokenExchangeMsi"; -import { tracingClient } from "../../util/tracing"; - -const logger = credentialLogger("ManagedIdentityCredential(MSAL)"); - -/** - * Options to send on the {@link ManagedIdentityCredential} constructor. - * Since this is an internal implementation, uses a looser interface than the public one. - */ -interface ManagedIdentityCredentialOptions extends TokenCredentialOptions { - /** - * The client ID of the user - assigned identity, or app registration(when working with AKS pod - identity). - */ - clientId?: string; - - /** - * Allows specifying a custom resource Id. - * In scenarios such as when user assigned identities are created using an ARM template, - * where the resource Id of the identity is known but the client Id can't be known ahead of time, - * this parameter allows programs to use these user assigned identities - * without having to first determine the client Id of the created identity. - */ - resourceId?: string; - - /** - * Allows specifying the object ID of the underlying service principal used to authenticate a user-assigned managed identity. - * This is an alternative to providing a client ID and is not required for system-assigned managed identities. - */ - objectId?: string; -} - -export class MsalMsiProvider { - private managedIdentityApp: ManagedIdentityApplication; - private identityClient: IdentityClient; - private clientId?: string; - private resourceId?: string; - private objectId?: string; - private msiRetryConfig: MSIConfiguration["retryConfig"] = { - maxRetries: 5, - startDelayInMs: 800, - intervalIncrement: 2, - }; - private isAvailableIdentityClient: IdentityClient; - - constructor( - clientIdOrOptions?: string | ManagedIdentityCredentialOptions, - options: ManagedIdentityCredentialOptions = {}, - ) { - let _options: ManagedIdentityCredentialOptions = {}; - 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 = [this.clientId, this.resourceId, this.objectId].filter(Boolean); - 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({ - ..._options, - additionalPolicies: [{ policy: imdsRetryPolicy(this.msiRetryConfig), position: "perCall" }], - }); - - this.managedIdentityApp = new ManagedIdentityApplication({ - managedIdentityIdParams: { - userAssignedClientId: this.clientId, - userAssignedResourceId: this.resourceId, - userAssignedObjectId: this.objectId, - }, - system: { - // todo: proxyUrl? - disableInternalRetries: true, - networkClient: this.identityClient, - loggerOptions: { - logLevel: getMSALLogLevel(getLogLevel()), - piiLoggingEnabled: options.loggingOptions?.enableUnsafeSupportLogging, - loggerCallback: defaultLoggerCallback(logger), - }, - }, - }); - - this.isAvailableIdentityClient = new IdentityClient({ - ..._options, - retryOptions: { - maxRetries: 0, - }, - }); - - // CloudShell MSI will ignore any user-assigned identity passed as parameters. To avoid confusion, we prevent this from happening as early as possible. - if (this.managedIdentityApp.getManagedIdentitySource() === "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.", - ); - } - } - } - - /** - * 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. - */ - public async getToken( - scopes: string | string[], - options: GetTokenOptions = {}, - ): Promise { - 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 () => { - try { - const isTokenExchangeMsi = await tokenExchangeMsi.isAvailable({ - scopes, - clientId: this.clientId, - getTokenOptions: options, - identityClient: this.identityClient, - resourceId: this.resourceId, - }); - - // 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: token.refreshOn?.getTime(), - tokenType: "Bearer", - } as AccessToken; - } catch (err: any) { - 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 - */ - private ensureValidMsalToken( - scopes: string | string[], - msalToken?: MsalToken, - getTokenOptions?: GetTokenOptions, - ): asserts msalToken is ValidMsalToken { - const createError = (message: string): Error => { - 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: any): boolean { - // 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; -} diff --git a/sdk/identity/identity/test/internal/node/azureApplicationCredential.spec.ts b/sdk/identity/identity/test/internal/node/azureApplicationCredential.spec.ts index 3cab08141d7a..6e060b9b8a72 100644 --- a/sdk/identity/identity/test/internal/node/azureApplicationCredential.spec.ts +++ b/sdk/identity/identity/test/internal/node/azureApplicationCredential.spec.ts @@ -9,7 +9,7 @@ import { IdentityTestContext } from "../../httpRequests"; import { RestError } from "@azure/core-rest-pipeline"; import { assert } from "chai"; import * as dac from "../../../src/credentials/defaultAzureCredential"; -import { LegacyMsiProvider } from "../../../src/credentials/managedIdentityCredential/legacyMsiProvider"; +import { ManagedIdentityCredential } from "../../../src/credentials/managedIdentityCredential/index"; describe("AzureApplicationCredential testing Managed Identity (internal)", function () { let envCopy: string = ""; @@ -25,7 +25,8 @@ describe("AzureApplicationCredential testing Managed Identity (internal)", funct testContext.sandbox .stub(dac, "createDefaultManagedIdentityCredential") .callsFake( - (...args) => new LegacyMsiProvider({ ...args, clientId: process.env.AZURE_CLIENT_ID }), + (...args) => + new ManagedIdentityCredential({ ...args, clientId: process.env.AZURE_CLIENT_ID }), ); }); diff --git a/sdk/identity/identity/test/internal/node/legacyMsiProvider.spec.ts b/sdk/identity/identity/test/internal/node/legacyMsiProvider.spec.ts index a22b5b88be5b..3f24c43df415 100644 --- a/sdk/identity/identity/test/internal/node/legacyMsiProvider.spec.ts +++ b/sdk/identity/identity/test/internal/node/legacyMsiProvider.spec.ts @@ -1,1066 +1,1066 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import * as arcMsi from "../../../src/credentials/managedIdentityCredential/arcMsi"; - -import { AzureLogger, setLogLevel } from "@azure/logger"; -import type { IdentityTestContextInterface } from "../../httpRequestsCommon"; -import { createResponse } from "../../httpRequestsCommon"; -import { - imdsApiVersion, - imdsEndpointPath, - imdsHost, -} from "../../../src/credentials/managedIdentityCredential/constants"; - -import type { Context } from "mocha"; -import type { GetTokenOptions } from "@azure/core-auth"; -import { IdentityTestContext } from "../../httpRequests"; -import { LegacyMsiProvider } from "../../../src/credentials/managedIdentityCredential/legacyMsiProvider"; -import { RestError } from "@azure/core-rest-pipeline"; -import Sinon from "sinon"; -import { assert } from "chai"; -import fs from "node:fs"; -import { imdsMsi } from "../../../src/credentials/managedIdentityCredential/imdsMsi"; -import { join } from "path"; -import { logger } from "../../../src/credentials/managedIdentityCredential/cloudShellMsi"; - -describe("ManagedIdentityCredential", function () { - let testContext: IdentityTestContextInterface; - let envCopy: string = ""; - - beforeEach(async function () { - envCopy = JSON.stringify(process.env); - delete process.env.AZURE_CLIENT_ID; - delete process.env.AZURE_TENANT_ID; - delete process.env.AZURE_CLIENT_SECRET; - delete process.env.IDENTITY_ENDPOINT; - delete process.env.IDENTITY_HEADER; - delete process.env.MSI_ENDPOINT; - delete process.env.MSI_SECRET; - delete process.env.IDENTITY_SERVER_THUMBPRINT; - delete process.env.IMDS_ENDPOINT; - delete process.env.AZURE_AUTHORITY_HOST; - delete process.env.AZURE_POD_IDENTITY_AUTHORITY_HOST; - delete process.env.AZURE_FEDERATED_TOKEN_FILE; - testContext = new IdentityTestContext({}); - }); - - afterEach(async function () { - const env = JSON.parse(envCopy); - // Useful for record mode. - process.env.AZURE_CLIENT_ID = env.AZURE_CLIENT_ID; - process.env.AZURE_TENANT_ID = env.AZURE_TENANT_ID; - process.env.AZURE_CLIENT_SECRET = env.AZURE_CLIENT_SECRET; - await testContext.restore(); - }); - - it("sends an authorization request with a modified resource name", async function () { - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential: new LegacyMsiProvider("client", { - authorityHost: "https://login.microsoftonline.com", - }), - insecureResponses: [ - createResponse(200), // IMDS Endpoint ping - createResponse(200, { - access_token: "token", - expires_on: "1506484173", - }), - ], - }); - - // The first request is the IMDS ping. - // This ping request has to skip a header and the query parameters for it to work on POD identity. - const imdsPingRequest = authDetails.requests[0]; - assert.ok(!imdsPingRequest.headers!.metadata); - assert.equal(imdsPingRequest.url, new URL(imdsEndpointPath, imdsHost).toString()); - - // The second one tries to authenticate against IMDS once we know the endpoint is available. - const authRequest = authDetails.requests[1]; - - const query = new URLSearchParams(authRequest.url.split("?")[1]); - - assert.equal(authRequest.method, "GET"); - assert.equal(query.get("client_id"), "client"); - assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); - assert.ok(authRequest.url.startsWith(imdsHost), "URL does not start with expected host"); - assert.ok( - authRequest.url.indexOf(`api-version=${imdsApiVersion}`) > -1, - "URL does not have expected version", - ); - }); - - it("sends an authorization request with an unmodified resource name", async () => { - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["someResource"], - credential: new LegacyMsiProvider(), - insecureResponses: [ - createResponse(200), // IMDS Endpoint ping - createResponse(200, { - access_token: "token", - expires_on: "1506484173", - }), - ], - }); - - // The first request is the IMDS ping. - // The second one tries to authenticate against IMDS once we know the endpoint is available. - const authRequest = authDetails.requests[1]; - - const query = new URLSearchParams(authRequest.url.split("?")[1]); - - assert.equal(query.get("client_id"), undefined); - assert.equal(decodeURIComponent(query.get("resource")!), "someResource"); - }); - - it("sends an authorization request with allowLoggingAccountIdentifiers set to true", async function () { - setLogLevel("info"); - const spy = testContext.sandbox.spy(process.stderr, "write"); - - const accessTokenData = { - appid: "HIDDEN", - tid: "HIDDEN", - oid: "HIDDEN", - }; - const base64AccessTokenData = Buffer.from(JSON.stringify(accessTokenData), "utf8").toString( - "base64", - ); - - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential: new LegacyMsiProvider("client", { - loggingOptions: { allowLoggingAccountIdentifiers: true }, - }), - insecureResponses: [ - createResponse(200), // IMDS Endpoint ping - createResponse(200, { - access_token: `token.${base64AccessTokenData}`, - expires_on: "1506484173", - }), - ], - }); - - // The first request is the IMDS ping. - // This ping request has to skip a header and the query parameters for it to work on POD identity. - const imdsPingRequest = authDetails.requests[0]; - assert.ok(!imdsPingRequest.headers!.metadata); - assert.equal(imdsPingRequest.url, new URL(imdsEndpointPath, imdsHost).toString()); - - // The first request is the IMDS ping. - // The second one tries to authenticate against IMDS once we know the endpoint is available. - const authRequest = authDetails.requests[1]; - const query = new URLSearchParams(authRequest.url.split("?")[1]); - - assert.equal(authRequest.method, "GET"); - assert.equal(query.get("client_id"), "client"); - assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); - assert.ok(authRequest.url.startsWith(imdsHost), "URL does not start with expected host"); - assert.ok( - authRequest.url.indexOf(`api-version=${imdsApiVersion}`) > -1, - "URL does not have expected version", - ); - const expectedMessage = `azure:identity:info ManagedIdentityCredential => getToken() => SUCCESS. Scopes: https://service/.default.`; - assert.equal((spy.getCall(spy.callCount - 2).args[0] as any as string).trim(), expectedMessage); - AzureLogger.destroy(); - }); - - it("sends an authorization request with tenantId on getToken", async () => { - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["someResource"], - getTokenOptions: { tenantId: "TENANT-ID" } as GetTokenOptions, - credential: new LegacyMsiProvider(), - insecureResponses: [ - createResponse(200), // IMDS Endpoint ping - createResponse(200, { - access_token: "token", - expires_on: "1506484173", - }), - ], - }); - - // The first request is the IMDS ping. - // The second one tries to authenticate against IMDS once we know the endpoint is available. - const authRequest = authDetails.requests[1]; - - const query = new URLSearchParams(authRequest.url.split("?")[1]); - - assert.equal(query.get("client_id"), undefined); - assert.equal(decodeURIComponent(query.get("resource")!), "someResource"); - }); - - it("returns error when no MSI is available", async function () { - process.env.AZURE_CLIENT_ID = "errclient"; - - const { error } = await testContext.sendCredentialRequests({ - scopes: ["scopes"], - credential: new LegacyMsiProvider(process.env.AZURE_CLIENT_ID), - insecureResponses: [ - { - error: new RestError("Request Timeout", { code: "REQUEST_SEND_ERROR", statusCode: 408 }), - }, - ], - }); - assert.ok( - error!.message!.indexOf("No MSI credential available") > -1, - "Failed to match the expected error", - ); - }); - - it("an unexpected error bubbles all the way up", async function () { - process.env.AZURE_CLIENT_ID = "errclient"; - const errorMessage = "ManagedIdentityCredential authentication failed."; - - const { error } = await testContext.sendCredentialRequests({ - scopes: ["scopes"], - credential: new LegacyMsiProvider(process.env.AZURE_CLIENT_ID), - insecureResponses: [ - createResponse(200), // IMDS Endpoint ping - { error: new RestError(errorMessage, { statusCode: 500 }) }, - ], - }); - assert.ok(error?.message.startsWith(errorMessage)); - }); - - it("returns expected error when the network was unreachable", async function () { - process.env.AZURE_CLIENT_ID = "errclient"; - - const netError: RestError = new RestError("Request Timeout", { - code: "ENETUNREACH", - statusCode: 408, - }); - - const { error } = await testContext.sendCredentialRequests({ - scopes: ["scopes"], - credential: new LegacyMsiProvider(process.env.AZURE_CLIENT_ID), - insecureResponses: [ - createResponse(200), // IMDS Endpoint ping - { error: netError }, - ], - }); - assert.ok(error!.message!.indexOf("Network unreachable.") > -1); - }); - - it("returns expected error when the host was unreachable", async function () { - process.env.AZURE_CLIENT_ID = "errclient"; - - const hostError: RestError = new RestError("Request Timeout", { - code: "EHOSTUNREACH", - statusCode: 408, - }); - - const { error } = await testContext.sendCredentialRequests({ - scopes: ["scopes"], - credential: new LegacyMsiProvider(process.env.AZURE_CLIENT_ID), - insecureResponses: [ - createResponse(200), // IMDS Endpoint ping - { error: hostError }, - ], - }); - assert.ok(error!.message!.indexOf("No managed identity endpoint found.") > -1); - }); - - it("returns expected error when the Docker Desktop IMDS responds for unreachable network", async function () { - const netError: RestError = new RestError( - "connecting to 169.254.169.254:80: connecting to 169.254.169.254:80: dial tcp 169.254.169.254:80: connectex: A socket operation was attempted to an unreachable network.", - { - statusCode: 403, - }, - ); - - const { error } = await testContext.sendCredentialRequests({ - scopes: ["scopes"], - credential: new LegacyMsiProvider(), - insecureResponses: [ - createResponse(200), // IMDS Endpoint ping - { error: netError }, - ], - }); - - assert.ok(error!.message!.indexOf("Network unreachable.") > -1); - assert(error!.name, "CredentialUnavailableError"); - }); - - it("IMDS MSI returns error on 403", async function () { - const { error } = await testContext.sendCredentialRequests({ - scopes: ["scopes"], - credential: new LegacyMsiProvider("errclient"), - insecureResponses: [ - createResponse(403, { - message: - "connecting to 169.254.169.254:80: connecting to 169.254.169.254:80: dial tcp 169.254.169.254:80: connectex: A socket operation was attempted to an unreachable network.", - }), - ], - }); - - assert.ok(error!.message.indexOf("No MSI credential available") > -1); - assert(error!.name, "CredentialUnavailableError"); - }); - it("IMDS MSI retries and succeeds on 404", async function () { - const { result, error } = await testContext.sendCredentialRequests({ - scopes: ["scopes"], - credential: new LegacyMsiProvider("errclient", { - authorityHost: "https://login.microsoftonline.com", - }), - insecureResponses: [ - createResponse(200), - createResponse(404), - createResponse(404), - createResponse(200, { - access_token: "token", - expires_on: "1506484173", - }), - ], - }); - assert.isUndefined(error); - assert.equal(result?.token, "token"); - }); - - it("IMDS MSI retries up to a limit on 404", async function () { - const credential = new LegacyMsiProvider("errclient"); - const { error } = await testContext.sendCredentialRequests({ - scopes: ["scopes"], - credential: credential, - insecureResponses: [ - createResponse(200), - createResponse(404), - createResponse(404), - createResponse(404), - createResponse(404), - createResponse(404), - ], - }); - assert.match(error!.message, /Failed to retrieve IMDS token after \d+ retries./); - }); - - it("IMDS MSI retries also retries on 503s", async function () { - const { result, error } = await testContext.sendCredentialRequests({ - scopes: ["scopes"], - credential: new LegacyMsiProvider("errclient"), - insecureResponses: [ - // Any response on the ping request is fine, since it means that the endpoint is indeed there. - createResponse(503), - // After the ping, we try to get a token from the IMDS endpoint. - createResponse(503, {}, { "Retry-After": "2" }), - createResponse(503, {}, { "Retry-After": "2" }), - createResponse(503, {}, { "Retry-After": "2" }), - createResponse(200, { access_token: "token", expires_on: 1506484173 }), - ], - }); - - assert.isUndefined(error); - assert.equal(result?.token, "token"); - }); - - it("IMDS MSI retries also retries on 500s", async function () { - const { result, error } = await testContext.sendCredentialRequests({ - scopes: ["scopes"], - credential: new LegacyMsiProvider("errclient"), - insecureResponses: [ - // Any response on the ping request is fine, since it means that the endpoint is indeed there. - createResponse(500, {}), - // After the ping, we try to get a token from the IMDS endpoint. - createResponse(500, {}), - createResponse(500, {}), - createResponse(500, {}), - createResponse(200, { access_token: "token", expires_on: "1506484173" }), - ], - }); - - assert.isUndefined(error); - assert.equal(result?.token, "token"); - }); - - it("IMDS MSI stops after 3 retries if the ping always gets 503s", async function () { - const { error } = await testContext.sendCredentialRequests({ - scopes: ["scopes"], - credential: new LegacyMsiProvider("errclient"), - insecureResponses: [ - // Any response on the ping request is fine, since it means that the endpoint is indeed there. - createResponse(503, {}, { "Retry-After": "2" }), - // After the ping, we try to get a token from the IMDS endpoint. - createResponse(503, {}, { "Retry-After": "2" }), - createResponse(503, {}, { "Retry-After": "2" }), - createResponse(503, {}, { "Retry-After": "2" }), - createResponse(503, {}, { "Retry-After": "2" }), - ], - }); - - assert.ok(error?.message); - assert.equal( - error?.message.split("\n")[0], - "ManagedIdentityCredential authentication failed. Status code: 503", - ); - }); - - it("IMDS MSI stops after 3 retries if the ping always gets 500s", async function () { - const { error } = await testContext.sendCredentialRequests({ - scopes: ["scopes"], - credential: new LegacyMsiProvider("errclient"), - insecureResponses: [ - // Any response on the ping request is fine, since it means that the endpoint is indeed there. - createResponse(500, {}), - // After the ping, we try to get a token from the IMDS endpoint. - createResponse(500, {}), - createResponse(500, {}), - createResponse(500, {}), - createResponse(500, {}), - ], - }); - - assert.ok(error?.message); - assert.equal( - error?.message.split("\n")[0], - "ManagedIdentityCredential authentication failed. Status code: 500", - ); - }); - - it("IMDS MSI accepts a custom set of retries, even when client Id is passed through the first parameter", async function () { - const { error } = await testContext.sendCredentialRequests({ - scopes: ["scopes"], - credential: new LegacyMsiProvider("errclient", { - retryOptions: { - maxRetries: 4, - }, - }), - insecureResponses: [ - // Any response on the ping request is fine, since it means that the endpoint is indeed there. - createResponse(503, {}, { "Retry-After": "2" }), - // After the ping, we try to get a token from the IMDS endpoint. - createResponse(503, {}, { "Retry-After": "2" }), - createResponse(503, {}, { "Retry-After": "2" }), - createResponse(503, {}, { "Retry-After": "2" }), - createResponse(503, {}, { "Retry-After": "2" }), - // This is the extra one - createResponse(503, {}, { "Retry-After": "2" }), - ], - }); - - assert.ok(error?.message); - assert.equal( - error?.message.split("\n")[0], - "ManagedIdentityCredential authentication failed. Status code: 503", - ); - }); - - it("IMDS MSI accepts a custom set of retries, even when client Id is not passed through the first parameter", async function () { - const { error } = await testContext.sendCredentialRequests({ - scopes: ["scopes"], - credential: new LegacyMsiProvider({ - retryOptions: { - maxRetries: 4, - }, - }), - insecureResponses: [ - // Any response on the ping request is fine, since it means that the endpoint is indeed there. - createResponse(503, {}, { "Retry-After": "2" }), - // After the ping, we try to get a token from the IMDS endpoint. - createResponse(503, {}, { "Retry-After": "2" }), - createResponse(503, {}, { "Retry-After": "2" }), - createResponse(503, {}, { "Retry-After": "2" }), - createResponse(503, {}, { "Retry-After": "2" }), - // This is the extra one - createResponse(503, {}, { "Retry-After": "2" }), - ], - }); - - assert.ok(error?.message); - assert.equal( - error?.message.split("\n")[0], - "ManagedIdentityCredential authentication failed. Status code: 503", - ); - }); - - it("IMDS MSI skips verification if the AZURE_POD_IDENTITY_AUTHORITY_HOST environment variable is available", async function () { - process.env.AZURE_POD_IDENTITY_AUTHORITY_HOST = "token URL"; - - assert.ok( - await imdsMsi.isAvailable({ - scopes: "https://endpoint/.default", - }), - ); - }); - - it("IMDS MSI works even if the AZURE_POD_IDENTITY_AUTHORITY_HOST ends with a slash", async function () { - process.env.AZURE_POD_IDENTITY_AUTHORITY_HOST = "http://10.0.0.1/"; - - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential: new LegacyMsiProvider({ - resourceId: "resource-id", - }), - insecureResponses: [ - createResponse(200, { - access_token: "token", - expires_on: "1506484173", - }), - ], - }); - - // The first request is the IMDS ping. - const imdsPingRequest = authDetails.requests[0]; - assert.equal( - imdsPingRequest.url, - "http://10.0.0.1/metadata/identity/oauth2/token?resource=https%3A%2F%2Fservice&api-version=2018-02-01&msi_res_id=resource-id", - ); - }); - - it("IMDS MSI works even if the AZURE_POD_IDENTITY_AUTHORITY_HOST doesn't end with a slash", async function () { - process.env.AZURE_POD_IDENTITY_AUTHORITY_HOST = "http://10.0.0.1"; - - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential: new LegacyMsiProvider("client"), - insecureResponses: [ - createResponse(200, { - access_token: "token", - expires_on: "1506484173", - }), - ], - }); - - // The first request is the IMDS ping. - const imdsPingRequest = authDetails.requests[0]; - - assert.equal( - imdsPingRequest.url, - "http://10.0.0.1/metadata/identity/oauth2/token?resource=https%3A%2F%2Fservice&api-version=2018-02-01&client_id=client", - ); - }); - - it("doesn't try IMDS endpoint again once it can't be detected", async function () { - const credential = new LegacyMsiProvider("errclient"); - const DEFAULT_CLIENT_MAX_RETRY_COUNT = 3; - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["scopes"], - credential, - insecureResponses: [ - // Satisfying the ping - createResponse(200), - // Retries until exhaustion - ...Array(DEFAULT_CLIENT_MAX_RETRY_COUNT + 1).fill( - createResponse(503, {}, { "Retry-After": "2" }), - ), - ], - }); - assert.equal(authDetails.requests.length, DEFAULT_CLIENT_MAX_RETRY_COUNT + 2); - assert.ok(authDetails.error!.message.indexOf("authentication failed") > -1); - - await testContext.restore(); - - const authDetails2 = await testContext.sendCredentialRequests({ - scopes: ["scopes"], - credential, - insecureResponses: [ - // This time, no ping should be triggered - createResponse(200, { access_token: "token", expires_on: "1506484173" }), - ], - }); - assert.isUndefined(authDetails2.error); - assert.equal(authDetails2.requests.length, 1); - assert.equal(authDetails2.result?.token, "token"); - }); - - it("sends an authorization request correctly in an App Service environment", async () => { - // Trigger App Service behavior by setting environment variables - process.env.MSI_ENDPOINT = "https://endpoint"; - process.env.MSI_SECRET = "secret"; - - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential: new LegacyMsiProvider("client"), - secureResponses: [ - createResponse(200, { - access_token: "token", - expires_on: "06/20/2019 02:57:58 +00:00", - }), - ], - }); - - const authRequest = authDetails.requests[0]; - const query = new URLSearchParams(authRequest.url.split("?")[1]); - - assert.equal(authRequest.method, "GET"); - assert.equal(query.get("clientid"), "client"); - assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); - assert.ok( - authRequest.url.startsWith(process.env.MSI_ENDPOINT), - "URL does not start with expected host and path", - ); - assert.equal(authRequest.headers.secret, process.env.MSI_SECRET); - assert.ok( - authRequest.url.indexOf(`api-version=2017-09-01`) > -1, - "URL does not have expected version", - ); - if (authDetails.result?.token) { - assert.equal(authDetails.result.expiresOnTimestamp, 1560999478000); - } else { - assert.fail("No token was returned!"); - } - }); - - it("sends an authorization request correctly in an App Service 2019 environment by client id", async () => { - // Trigger App Service behavior by setting environment variables - process.env.IDENTITY_ENDPOINT = "https://endpoint"; - process.env.IDENTITY_HEADER = "HEADER"; - - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential: new LegacyMsiProvider("client"), - secureResponses: [ - createResponse(200, { - access_token: "token", - expires_on: "1624157878", - }), - ], - }); - - const authRequest = authDetails.requests[0]; - const query = new URLSearchParams(authRequest.url.split("?")[1]); - - assert.equal(authRequest.method, "GET"); - assert.equal(query.get("client_id"), "client"); - assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); - assert.ok( - authRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), - "URL does not start with expected host and path", - ); - assert.equal(authRequest.headers["X-IDENTITY-HEADER"], process.env.IDENTITY_HEADER); - assert.ok( - authRequest.url.indexOf(`api-version=2019-08-01`) > -1, - "URL does not have expected version", - ); - if (authDetails.result?.token) { - assert.equal(authDetails.result.expiresOnTimestamp, 1624157878000); - } else { - assert.fail("No token was returned!"); - } - }); - - it("sends an authorization request correctly in an App Service 2019 environment by resource id", async () => { - // Trigger App Service behavior by setting environment variables - process.env.IDENTITY_ENDPOINT = "https://endpoint"; - process.env.IDENTITY_HEADER = "HEADER"; - - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential: new LegacyMsiProvider({ resourceId: "RESOURCE-ID" }), - secureResponses: [ - createResponse(200, { - access_token: "token", - expires_on: "1624157878", - }), - ], - }); - - const authRequest = authDetails.requests[0]; - const query = new URLSearchParams(authRequest.url.split("?")[1]); - - assert.equal(authRequest.method, "GET"); - assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); - assert.equal(decodeURIComponent(query.get("mi_res_id")!), "RESOURCE-ID"); - assert.ok( - authRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), - "URL does not start with expected host and path", - ); - assert.equal(authRequest.headers["X-IDENTITY-HEADER"], process.env.IDENTITY_HEADER); - assert.ok( - authRequest.url.indexOf(`api-version=2019-08-01`) > -1, - "URL does not have expected version", - ); - if (authDetails.result?.token) { - assert.equal(authDetails.result.expiresOnTimestamp, 1624157878000); - } else { - assert.fail("No token was returned!"); - } - }); - - it("sends an authorization request correctly in an Cloud Shell environment", async () => { - // Trigger Cloud Shell behavior by setting environment variables - process.env.MSI_ENDPOINT = "https://endpoint"; - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential: new LegacyMsiProvider(), - secureResponses: [ - createResponse(200, { - access_token: "token", - expires_in: "4310", - expires_on: "1663366555", - }), - ], - }); - const authRequest = authDetails.requests[0]; - assert.equal(authRequest.method, "POST"); - assert.equal(authDetails.result!.token, "token"); - }); - - it("sends an authorization request correctly in an Cloud Shell environment (with clientId)", async () => { - // Trigger Cloud Shell behavior by setting environment variables - process.env.MSI_ENDPOINT = "https://endpoint"; - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential: new LegacyMsiProvider({ clientId: "CLIENT-ID" }), - secureResponses: [ - createResponse(200, { - access_token: "token", - expires_in: "4310", - expires_on: "1663366555", - }), - ], - }); - const authRequest = authDetails.requests[0]; - const body = new URLSearchParams(authRequest.body); - assert.strictEqual(decodeURIComponent(body.get("client_id")!), "CLIENT-ID"); - assert.equal(authRequest.method, "POST"); - assert.equal(authDetails.result!.token, "token"); - }); - - it("sends an authorization request correctly in an Cloud Shell environment (with resourceId)", async () => { - // Trigger Cloud Shell behavior by setting environment variables - process.env.MSI_ENDPOINT = "https://endpoint"; - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential: new LegacyMsiProvider({ resourceId: "RESOURCE-ID" }), - secureResponses: [ - createResponse(200, { - access_token: "token", - expires_in: "4310", - expires_on: "1663366555", - }), - ], - }); - const authRequest = authDetails.requests[0]; - const body = new URLSearchParams(authRequest.body); - assert.strictEqual(decodeURIComponent(body.get("msi_res_id")!), "RESOURCE-ID"); - assert.equal(authRequest.method, "POST"); - assert.equal(authDetails.result!.token, "token"); - }); - - it("authorization request fails with client id passed in an Cloud Shell environment", async function (this: Context) { - // Trigger Cloud Shell behavior by setting environment variables - process.env.MSI_ENDPOINT = "https://endpoint"; - const msiGetTokenSpy = Sinon.spy(LegacyMsiProvider.prototype, "getToken"); - const loggerSpy = Sinon.spy(logger, "warning"); - setLogLevel("warning"); - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential: new LegacyMsiProvider("client"), - secureResponses: [ - createResponse(200, { - access_token: "token", - expires_in: "4310", - expires_on: "1663366555", - }), - ], - }); - assert.equal(authDetails.result!.token, "token"); - assert.equal(msiGetTokenSpy.called, true); - assert.equal(loggerSpy.calledOnce, true); - assert.deepEqual(loggerSpy.args[0], [ - "ManagedIdentityCredential - CloudShellMSI: user-assigned identities not supported. The argument clientId might be ignored by the service.", - ]); - }); - - describe("Azure Arc", function () { - const keyContents = "challenge key"; - let expectedDirectory: string; - - beforeEach(function () { - if (process.platform !== "win32" && process.platform !== "linux") { - // not supported on this platform - this.skip(); - } - expectedDirectory = arcMsi.platformToFilePath(); - - // Trigger Azure Arc behavior by setting environment variables - process.env.IMDS_ENDPOINT = "http://endpoint"; - process.env.IDENTITY_ENDPOINT = "http://endpoint"; - // Stub out a valid key file - Sinon.stub(fs, "statSync").returns({ size: 400 } as any); - Sinon.stub(fs.promises, "readFile").resolves(keyContents); - }); - - afterEach(function () { - Sinon.restore(); - }); - - it("sends an authorization request correctly in an Azure Arc environment", async function (this: Mocha.Context) { - const tempFile = join(expectedDirectory, "fake.key"); - - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential: new LegacyMsiProvider(), - insecureResponses: [ - createResponse( - 401, - {}, - { - "www-authenticate": `we don't pay much attention about this format=${tempFile}`, - }, - ), - createResponse(200, { - access_token: "token", - expires_in: 1, - }), - ], - }); - - // File request - const validationRequest = authDetails.requests[0]; - let query = new URLSearchParams(validationRequest.url.split("?")[1]); - - assert.equal(validationRequest.method, "GET"); - assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); - - assert.exists(process.env.IDENTITY_ENDPOINT); - assert.ok( - validationRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), - "URL does not start with expected host and path", - ); - - // Authorization request, which comes after getting the file path, for now at least. - const authRequest = authDetails.requests[1]; - query = new URLSearchParams(authRequest.url.split("?")[1]); - - assert.equal(authRequest.method, "GET"); - assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); - - assert.ok( - authRequest.url.startsWith(process.env.IDENTITY_ENDPOINT!), - "URL does not start with expected host and path", - ); - - assert.equal(authRequest.headers.Authorization, `Basic ${keyContents}`); - if (authDetails.result!.token) { - // We use Date.now underneath. - assert.ok(authDetails.result!.expiresOnTimestamp); - } else { - assert.fail("No token was returned!"); - } - }); - - it("sends an authorization request correctly in an Azure Arc environment (with resourceId)", async function (this: Mocha.Context) { - const filePath = join(expectedDirectory, "fake.key"); - - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential: new LegacyMsiProvider({ resourceId: "RESOURCE-ID" }), - insecureResponses: [ - createResponse( - 401, - {}, - { - "www-authenticate": `we don't pay much attention about this format=${filePath}`, - }, - ), - createResponse(200, { - access_token: "token", - expires_in: 1, - }), - ], - }); - - // File request - const validationRequest = authDetails.requests[0]; - let query = new URLSearchParams(validationRequest.url.split("?")[1]); - - assert.equal(validationRequest.method, "GET"); - assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); - - assert.exists(process.env.IDENTITY_ENDPOINT); - assert.ok( - validationRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), - "URL does not start with expected host and path", - ); - - // Authorization request, which comes after getting the file path, for now at least. - const authRequest = authDetails.requests[1]; - query = new URLSearchParams(authRequest.url.split("?")[1]); - - assert.equal(authRequest.method, "GET"); - assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); - assert.equal(decodeURIComponent(query.get("msi_res_id")!), "RESOURCE-ID"); - - assert.ok( - authRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), - "URL does not start with expected host and path", - ); - - assert.equal(authRequest.headers.Authorization, `Basic ${keyContents}`); - if (authDetails.result!.token) { - // We use Date.now underneath. - assert.ok(authDetails.result!.expiresOnTimestamp); - } else { - assert.fail("No token was returned!"); - } - }); - - it("sends an authorization request correctly in an Azure Arc environment (with clientId)", async function (this: Mocha.Context) { - const filePath = join(expectedDirectory, "fake.key"); - - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential: new LegacyMsiProvider({ clientId: "CLIENT-ID" }), - insecureResponses: [ - createResponse( - 401, - {}, - { - "www-authenticate": `we don't pay much attention about this format=${filePath}`, - }, - ), - createResponse(200, { - access_token: "token", - expires_in: 1, - }), - ], - }); - - // File request - const validationRequest = authDetails.requests[0]; - console.log(validationRequest.url.split("?")[1]); - let query = new URLSearchParams(validationRequest.url.split("?")[1]); - - assert.equal(validationRequest.method, "GET"); - assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); - - assert.exists(process.env.IDENTITY_ENDPOINT); - assert.ok( - validationRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), - "URL does not start with expected host and path", - ); - - // Authorization request, which comes after getting the file path, for now at least. - const authRequest = authDetails.requests[1]; - console.log(authRequest.url.split("?")[1]); - query = new URLSearchParams(authRequest.url.split("?")[1]); - - assert.equal(authRequest.method, "GET"); - assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); - assert.equal(decodeURIComponent(query.get("client_id")!), "CLIENT-ID"); - - assert.ok( - authRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), - "URL does not start with expected host and path", - ); - - assert.equal(authRequest.headers.Authorization, `Basic ${keyContents}`); - if (authDetails.result!.token) { - // We use Date.now underneath. - assert.ok(authDetails.result!.expiresOnTimestamp); - } else { - assert.fail("No token was returned!"); - } - }); - }); - - it("sends an authorization request correctly in an Azure Fabric environment", async () => { - // Trigger App Service behavior by setting environment variables - process.env.IDENTITY_ENDPOINT = "https://endpoint"; - process.env.IDENTITY_HEADER = "secret"; - - // We're not verifying the certificate yet, but we still check for it: - process.env.IDENTITY_SERVER_THUMBPRINT = "certificate-thumbprint"; - - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential: new LegacyMsiProvider("client"), - secureResponses: [ - createResponse(200, { - access_token: "token", - expires_on: 1, - }), - ], - }); - - // Authorization request, which comes after validating again, for now at least. - const authRequest = authDetails.requests[0]; - - const query = new URLSearchParams(authRequest.url.split("?")[1]); - - assert.equal(authRequest.method, "GET"); - assert.equal(query.get("client_id"), "client"); - assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); - assert.ok( - authRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), - "URL does not start with expected host and path", - ); - - assert.equal(authRequest.headers.secret, process.env.IDENTITY_HEADER); - - if (authDetails.result!.token) { - // We use Date.now underneath. - assert.equal(authDetails.result!.expiresOnTimestamp, 1000); - } else { - assert.fail("No token was returned!"); - } - }); - - it("sends an authorization request correctly in an Azure Fabric environment (resourceId)", async () => { - // Trigger App Service behavior by setting environment variables - process.env.IDENTITY_ENDPOINT = "https://endpoint"; - process.env.IDENTITY_HEADER = "secret"; - - // We're not verifying the certificate yet, but we still check for it: - process.env.IDENTITY_SERVER_THUMBPRINT = "certificate-thumbprint"; - - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential: new LegacyMsiProvider({ resourceId: "RESOURCE-ID" }), - secureResponses: [ - createResponse(200, { - access_token: "token", - expires_on: 1, - }), - ], - }); - - // Authorization request, which comes after validating again, for now at least. - const authRequest = authDetails.requests[0]; - - const query = new URLSearchParams(authRequest.url.split("?")[1]); - - assert.equal(authRequest.method, "GET"); - assert.equal(query.get("msi_res_id"), "RESOURCE-ID"); - assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); - assert.ok( - authRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), - "URL does not start with expected host and path", - ); - - assert.equal(authRequest.headers.secret, process.env.IDENTITY_HEADER); - - if (authDetails.result!.token) { - // We use Date.now underneath. - assert.equal(authDetails.result!.expiresOnTimestamp, 1000); - } else { - assert.fail("No token was returned!"); - } - }); - - it("calls to AppTokenProvider for MI token caching support", async () => { - const credential: any = new LegacyMsiProvider("client"); - const confidentialSpy = Sinon.spy(credential.confidentialApp, "SetAppTokenProvider"); - - // Trigger App Service behavior by setting environment variables - process.env.MSI_ENDPOINT = "https://endpoint"; - process.env.MSI_SECRET = "secret"; - - const authDetails = await testContext.sendCredentialRequests({ - scopes: ["https://service/.default"], - credential, - secureResponses: [ - createResponse(200, { - access_token: "token", - expires_on: "06/20/2019 02:57:58 +00:00", - }), - ], - }); - assert.equal(confidentialSpy.callCount, 1); - - if (authDetails.result?.token) { - assert.equal(authDetails.result.expiresOnTimestamp, 1560999478000); - } else { - assert.fail("No token was returned!"); - } - }); -}); +// import * as arcMsi from "../../../src/credentials/managedIdentityCredential/arcMsi"; + +// import { AzureLogger, setLogLevel } from "@azure/logger"; +// import type { IdentityTestContextInterface } from "../../httpRequestsCommon"; +// import { createResponse } from "../../httpRequestsCommon"; +// import { +// imdsApiVersion, +// imdsEndpointPath, +// imdsHost, +// } from "../../../src/credentials/managedIdentityCredential/constants"; + +// import type { Context } from "mocha"; +// import type { GetTokenOptions } from "@azure/core-auth"; +// import { IdentityTestContext } from "../../httpRequests"; +// import { LegacyMsiProvider } from "../../../src/credentials/managedIdentityCredential/legacyMsiProvider"; +// import { RestError } from "@azure/core-rest-pipeline"; +// import Sinon from "sinon"; +// import { assert } from "chai"; +// import fs from "node:fs"; +// import { imdsMsi } from "../../../src/credentials/managedIdentityCredential/imdsMsi"; +// import { join } from "path"; +// import { logger } from "../../../src/credentials/managedIdentityCredential/cloudShellMsi"; + +// describe("ManagedIdentityCredential", function () { +// let testContext: IdentityTestContextInterface; +// let envCopy: string = ""; + +// beforeEach(async function () { +// envCopy = JSON.stringify(process.env); +// delete process.env.AZURE_CLIENT_ID; +// delete process.env.AZURE_TENANT_ID; +// delete process.env.AZURE_CLIENT_SECRET; +// delete process.env.IDENTITY_ENDPOINT; +// delete process.env.IDENTITY_HEADER; +// delete process.env.MSI_ENDPOINT; +// delete process.env.MSI_SECRET; +// delete process.env.IDENTITY_SERVER_THUMBPRINT; +// delete process.env.IMDS_ENDPOINT; +// delete process.env.AZURE_AUTHORITY_HOST; +// delete process.env.AZURE_POD_IDENTITY_AUTHORITY_HOST; +// delete process.env.AZURE_FEDERATED_TOKEN_FILE; +// testContext = new IdentityTestContext({}); +// }); + +// afterEach(async function () { +// const env = JSON.parse(envCopy); +// // Useful for record mode. +// process.env.AZURE_CLIENT_ID = env.AZURE_CLIENT_ID; +// process.env.AZURE_TENANT_ID = env.AZURE_TENANT_ID; +// process.env.AZURE_CLIENT_SECRET = env.AZURE_CLIENT_SECRET; +// await testContext.restore(); +// }); + +// it("sends an authorization request with a modified resource name", async function () { +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential: new LegacyMsiProvider("client", { +// authorityHost: "https://login.microsoftonline.com", +// }), +// insecureResponses: [ +// createResponse(200), // IMDS Endpoint ping +// createResponse(200, { +// access_token: "token", +// expires_on: "1506484173", +// }), +// ], +// }); + +// // The first request is the IMDS ping. +// // This ping request has to skip a header and the query parameters for it to work on POD identity. +// const imdsPingRequest = authDetails.requests[0]; +// assert.ok(!imdsPingRequest.headers!.metadata); +// assert.equal(imdsPingRequest.url, new URL(imdsEndpointPath, imdsHost).toString()); + +// // The second one tries to authenticate against IMDS once we know the endpoint is available. +// const authRequest = authDetails.requests[1]; + +// const query = new URLSearchParams(authRequest.url.split("?")[1]); + +// assert.equal(authRequest.method, "GET"); +// assert.equal(query.get("client_id"), "client"); +// assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); +// assert.ok(authRequest.url.startsWith(imdsHost), "URL does not start with expected host"); +// assert.ok( +// authRequest.url.indexOf(`api-version=${imdsApiVersion}`) > -1, +// "URL does not have expected version", +// ); +// }); + +// it("sends an authorization request with an unmodified resource name", async () => { +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["someResource"], +// credential: new LegacyMsiProvider(), +// insecureResponses: [ +// createResponse(200), // IMDS Endpoint ping +// createResponse(200, { +// access_token: "token", +// expires_on: "1506484173", +// }), +// ], +// }); + +// // The first request is the IMDS ping. +// // The second one tries to authenticate against IMDS once we know the endpoint is available. +// const authRequest = authDetails.requests[1]; + +// const query = new URLSearchParams(authRequest.url.split("?")[1]); + +// assert.equal(query.get("client_id"), undefined); +// assert.equal(decodeURIComponent(query.get("resource")!), "someResource"); +// }); + +// it("sends an authorization request with allowLoggingAccountIdentifiers set to true", async function () { +// setLogLevel("info"); +// const spy = testContext.sandbox.spy(process.stderr, "write"); + +// const accessTokenData = { +// appid: "HIDDEN", +// tid: "HIDDEN", +// oid: "HIDDEN", +// }; +// const base64AccessTokenData = Buffer.from(JSON.stringify(accessTokenData), "utf8").toString( +// "base64", +// ); + +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential: new LegacyMsiProvider("client", { +// loggingOptions: { allowLoggingAccountIdentifiers: true }, +// }), +// insecureResponses: [ +// createResponse(200), // IMDS Endpoint ping +// createResponse(200, { +// access_token: `token.${base64AccessTokenData}`, +// expires_on: "1506484173", +// }), +// ], +// }); + +// // The first request is the IMDS ping. +// // This ping request has to skip a header and the query parameters for it to work on POD identity. +// const imdsPingRequest = authDetails.requests[0]; +// assert.ok(!imdsPingRequest.headers!.metadata); +// assert.equal(imdsPingRequest.url, new URL(imdsEndpointPath, imdsHost).toString()); + +// // The first request is the IMDS ping. +// // The second one tries to authenticate against IMDS once we know the endpoint is available. +// const authRequest = authDetails.requests[1]; +// const query = new URLSearchParams(authRequest.url.split("?")[1]); + +// assert.equal(authRequest.method, "GET"); +// assert.equal(query.get("client_id"), "client"); +// assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); +// assert.ok(authRequest.url.startsWith(imdsHost), "URL does not start with expected host"); +// assert.ok( +// authRequest.url.indexOf(`api-version=${imdsApiVersion}`) > -1, +// "URL does not have expected version", +// ); +// const expectedMessage = `azure:identity:info ManagedIdentityCredential => getToken() => SUCCESS. Scopes: https://service/.default.`; +// assert.equal((spy.getCall(spy.callCount - 2).args[0] as any as string).trim(), expectedMessage); +// AzureLogger.destroy(); +// }); + +// it("sends an authorization request with tenantId on getToken", async () => { +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["someResource"], +// getTokenOptions: { tenantId: "TENANT-ID" } as GetTokenOptions, +// credential: new LegacyMsiProvider(), +// insecureResponses: [ +// createResponse(200), // IMDS Endpoint ping +// createResponse(200, { +// access_token: "token", +// expires_on: "1506484173", +// }), +// ], +// }); + +// // The first request is the IMDS ping. +// // The second one tries to authenticate against IMDS once we know the endpoint is available. +// const authRequest = authDetails.requests[1]; + +// const query = new URLSearchParams(authRequest.url.split("?")[1]); + +// assert.equal(query.get("client_id"), undefined); +// assert.equal(decodeURIComponent(query.get("resource")!), "someResource"); +// }); + +// it("returns error when no MSI is available", async function () { +// process.env.AZURE_CLIENT_ID = "errclient"; + +// const { error } = await testContext.sendCredentialRequests({ +// scopes: ["scopes"], +// credential: new LegacyMsiProvider(process.env.AZURE_CLIENT_ID), +// insecureResponses: [ +// { +// error: new RestError("Request Timeout", { code: "REQUEST_SEND_ERROR", statusCode: 408 }), +// }, +// ], +// }); +// assert.ok( +// error!.message!.indexOf("No MSI credential available") > -1, +// "Failed to match the expected error", +// ); +// }); + +// it("an unexpected error bubbles all the way up", async function () { +// process.env.AZURE_CLIENT_ID = "errclient"; +// const errorMessage = "ManagedIdentityCredential authentication failed."; + +// const { error } = await testContext.sendCredentialRequests({ +// scopes: ["scopes"], +// credential: new LegacyMsiProvider(process.env.AZURE_CLIENT_ID), +// insecureResponses: [ +// createResponse(200), // IMDS Endpoint ping +// { error: new RestError(errorMessage, { statusCode: 500 }) }, +// ], +// }); +// assert.ok(error?.message.startsWith(errorMessage)); +// }); + +// it("returns expected error when the network was unreachable", async function () { +// process.env.AZURE_CLIENT_ID = "errclient"; + +// const netError: RestError = new RestError("Request Timeout", { +// code: "ENETUNREACH", +// statusCode: 408, +// }); + +// const { error } = await testContext.sendCredentialRequests({ +// scopes: ["scopes"], +// credential: new LegacyMsiProvider(process.env.AZURE_CLIENT_ID), +// insecureResponses: [ +// createResponse(200), // IMDS Endpoint ping +// { error: netError }, +// ], +// }); +// assert.ok(error!.message!.indexOf("Network unreachable.") > -1); +// }); + +// it("returns expected error when the host was unreachable", async function () { +// process.env.AZURE_CLIENT_ID = "errclient"; + +// const hostError: RestError = new RestError("Request Timeout", { +// code: "EHOSTUNREACH", +// statusCode: 408, +// }); + +// const { error } = await testContext.sendCredentialRequests({ +// scopes: ["scopes"], +// credential: new LegacyMsiProvider(process.env.AZURE_CLIENT_ID), +// insecureResponses: [ +// createResponse(200), // IMDS Endpoint ping +// { error: hostError }, +// ], +// }); +// assert.ok(error!.message!.indexOf("No managed identity endpoint found.") > -1); +// }); + +// it("returns expected error when the Docker Desktop IMDS responds for unreachable network", async function () { +// const netError: RestError = new RestError( +// "connecting to 169.254.169.254:80: connecting to 169.254.169.254:80: dial tcp 169.254.169.254:80: connectex: A socket operation was attempted to an unreachable network.", +// { +// statusCode: 403, +// }, +// ); + +// const { error } = await testContext.sendCredentialRequests({ +// scopes: ["scopes"], +// credential: new LegacyMsiProvider(), +// insecureResponses: [ +// createResponse(200), // IMDS Endpoint ping +// { error: netError }, +// ], +// }); + +// assert.ok(error!.message!.indexOf("Network unreachable.") > -1); +// assert(error!.name, "CredentialUnavailableError"); +// }); + +// it("IMDS MSI returns error on 403", async function () { +// const { error } = await testContext.sendCredentialRequests({ +// scopes: ["scopes"], +// credential: new LegacyMsiProvider("errclient"), +// insecureResponses: [ +// createResponse(403, { +// message: +// "connecting to 169.254.169.254:80: connecting to 169.254.169.254:80: dial tcp 169.254.169.254:80: connectex: A socket operation was attempted to an unreachable network.", +// }), +// ], +// }); + +// assert.ok(error!.message.indexOf("No MSI credential available") > -1); +// assert(error!.name, "CredentialUnavailableError"); +// }); +// it("IMDS MSI retries and succeeds on 404", async function () { +// const { result, error } = await testContext.sendCredentialRequests({ +// scopes: ["scopes"], +// credential: new LegacyMsiProvider("errclient", { +// authorityHost: "https://login.microsoftonline.com", +// }), +// insecureResponses: [ +// createResponse(200), +// createResponse(404), +// createResponse(404), +// createResponse(200, { +// access_token: "token", +// expires_on: "1506484173", +// }), +// ], +// }); +// assert.isUndefined(error); +// assert.equal(result?.token, "token"); +// }); + +// it("IMDS MSI retries up to a limit on 404", async function () { +// const credential = new LegacyMsiProvider("errclient"); +// const { error } = await testContext.sendCredentialRequests({ +// scopes: ["scopes"], +// credential: credential, +// insecureResponses: [ +// createResponse(200), +// createResponse(404), +// createResponse(404), +// createResponse(404), +// createResponse(404), +// createResponse(404), +// ], +// }); +// assert.match(error!.message, /Failed to retrieve IMDS token after \d+ retries./); +// }); + +// it("IMDS MSI retries also retries on 503s", async function () { +// const { result, error } = await testContext.sendCredentialRequests({ +// scopes: ["scopes"], +// credential: new LegacyMsiProvider("errclient"), +// insecureResponses: [ +// // Any response on the ping request is fine, since it means that the endpoint is indeed there. +// createResponse(503), +// // After the ping, we try to get a token from the IMDS endpoint. +// createResponse(503, {}, { "Retry-After": "2" }), +// createResponse(503, {}, { "Retry-After": "2" }), +// createResponse(503, {}, { "Retry-After": "2" }), +// createResponse(200, { access_token: "token", expires_on: 1506484173 }), +// ], +// }); + +// assert.isUndefined(error); +// assert.equal(result?.token, "token"); +// }); + +// it("IMDS MSI retries also retries on 500s", async function () { +// const { result, error } = await testContext.sendCredentialRequests({ +// scopes: ["scopes"], +// credential: new LegacyMsiProvider("errclient"), +// insecureResponses: [ +// // Any response on the ping request is fine, since it means that the endpoint is indeed there. +// createResponse(500, {}), +// // After the ping, we try to get a token from the IMDS endpoint. +// createResponse(500, {}), +// createResponse(500, {}), +// createResponse(500, {}), +// createResponse(200, { access_token: "token", expires_on: "1506484173" }), +// ], +// }); + +// assert.isUndefined(error); +// assert.equal(result?.token, "token"); +// }); + +// it("IMDS MSI stops after 3 retries if the ping always gets 503s", async function () { +// const { error } = await testContext.sendCredentialRequests({ +// scopes: ["scopes"], +// credential: new LegacyMsiProvider("errclient"), +// insecureResponses: [ +// // Any response on the ping request is fine, since it means that the endpoint is indeed there. +// createResponse(503, {}, { "Retry-After": "2" }), +// // After the ping, we try to get a token from the IMDS endpoint. +// createResponse(503, {}, { "Retry-After": "2" }), +// createResponse(503, {}, { "Retry-After": "2" }), +// createResponse(503, {}, { "Retry-After": "2" }), +// createResponse(503, {}, { "Retry-After": "2" }), +// ], +// }); + +// assert.ok(error?.message); +// assert.equal( +// error?.message.split("\n")[0], +// "ManagedIdentityCredential authentication failed. Status code: 503", +// ); +// }); + +// it("IMDS MSI stops after 3 retries if the ping always gets 500s", async function () { +// const { error } = await testContext.sendCredentialRequests({ +// scopes: ["scopes"], +// credential: new LegacyMsiProvider("errclient"), +// insecureResponses: [ +// // Any response on the ping request is fine, since it means that the endpoint is indeed there. +// createResponse(500, {}), +// // After the ping, we try to get a token from the IMDS endpoint. +// createResponse(500, {}), +// createResponse(500, {}), +// createResponse(500, {}), +// createResponse(500, {}), +// ], +// }); + +// assert.ok(error?.message); +// assert.equal( +// error?.message.split("\n")[0], +// "ManagedIdentityCredential authentication failed. Status code: 500", +// ); +// }); + +// it("IMDS MSI accepts a custom set of retries, even when client Id is passed through the first parameter", async function () { +// const { error } = await testContext.sendCredentialRequests({ +// scopes: ["scopes"], +// credential: new LegacyMsiProvider("errclient", { +// retryOptions: { +// maxRetries: 4, +// }, +// }), +// insecureResponses: [ +// // Any response on the ping request is fine, since it means that the endpoint is indeed there. +// createResponse(503, {}, { "Retry-After": "2" }), +// // After the ping, we try to get a token from the IMDS endpoint. +// createResponse(503, {}, { "Retry-After": "2" }), +// createResponse(503, {}, { "Retry-After": "2" }), +// createResponse(503, {}, { "Retry-After": "2" }), +// createResponse(503, {}, { "Retry-After": "2" }), +// // This is the extra one +// createResponse(503, {}, { "Retry-After": "2" }), +// ], +// }); + +// assert.ok(error?.message); +// assert.equal( +// error?.message.split("\n")[0], +// "ManagedIdentityCredential authentication failed. Status code: 503", +// ); +// }); + +// it("IMDS MSI accepts a custom set of retries, even when client Id is not passed through the first parameter", async function () { +// const { error } = await testContext.sendCredentialRequests({ +// scopes: ["scopes"], +// credential: new LegacyMsiProvider({ +// retryOptions: { +// maxRetries: 4, +// }, +// }), +// insecureResponses: [ +// // Any response on the ping request is fine, since it means that the endpoint is indeed there. +// createResponse(503, {}, { "Retry-After": "2" }), +// // After the ping, we try to get a token from the IMDS endpoint. +// createResponse(503, {}, { "Retry-After": "2" }), +// createResponse(503, {}, { "Retry-After": "2" }), +// createResponse(503, {}, { "Retry-After": "2" }), +// createResponse(503, {}, { "Retry-After": "2" }), +// // This is the extra one +// createResponse(503, {}, { "Retry-After": "2" }), +// ], +// }); + +// assert.ok(error?.message); +// assert.equal( +// error?.message.split("\n")[0], +// "ManagedIdentityCredential authentication failed. Status code: 503", +// ); +// }); + +// it("IMDS MSI skips verification if the AZURE_POD_IDENTITY_AUTHORITY_HOST environment variable is available", async function () { +// process.env.AZURE_POD_IDENTITY_AUTHORITY_HOST = "token URL"; + +// assert.ok( +// await imdsMsi.isAvailable({ +// scopes: "https://endpoint/.default", +// }), +// ); +// }); + +// it("IMDS MSI works even if the AZURE_POD_IDENTITY_AUTHORITY_HOST ends with a slash", async function () { +// process.env.AZURE_POD_IDENTITY_AUTHORITY_HOST = "http://10.0.0.1/"; + +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential: new LegacyMsiProvider({ +// resourceId: "resource-id", +// }), +// insecureResponses: [ +// createResponse(200, { +// access_token: "token", +// expires_on: "1506484173", +// }), +// ], +// }); + +// // The first request is the IMDS ping. +// const imdsPingRequest = authDetails.requests[0]; +// assert.equal( +// imdsPingRequest.url, +// "http://10.0.0.1/metadata/identity/oauth2/token?resource=https%3A%2F%2Fservice&api-version=2018-02-01&msi_res_id=resource-id", +// ); +// }); + +// it("IMDS MSI works even if the AZURE_POD_IDENTITY_AUTHORITY_HOST doesn't end with a slash", async function () { +// process.env.AZURE_POD_IDENTITY_AUTHORITY_HOST = "http://10.0.0.1"; + +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential: new LegacyMsiProvider("client"), +// insecureResponses: [ +// createResponse(200, { +// access_token: "token", +// expires_on: "1506484173", +// }), +// ], +// }); + +// // The first request is the IMDS ping. +// const imdsPingRequest = authDetails.requests[0]; + +// assert.equal( +// imdsPingRequest.url, +// "http://10.0.0.1/metadata/identity/oauth2/token?resource=https%3A%2F%2Fservice&api-version=2018-02-01&client_id=client", +// ); +// }); + +// it("doesn't try IMDS endpoint again once it can't be detected", async function () { +// const credential = new LegacyMsiProvider("errclient"); +// const DEFAULT_CLIENT_MAX_RETRY_COUNT = 3; +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["scopes"], +// credential, +// insecureResponses: [ +// // Satisfying the ping +// createResponse(200), +// // Retries until exhaustion +// ...Array(DEFAULT_CLIENT_MAX_RETRY_COUNT + 1).fill( +// createResponse(503, {}, { "Retry-After": "2" }), +// ), +// ], +// }); +// assert.equal(authDetails.requests.length, DEFAULT_CLIENT_MAX_RETRY_COUNT + 2); +// assert.ok(authDetails.error!.message.indexOf("authentication failed") > -1); + +// await testContext.restore(); + +// const authDetails2 = await testContext.sendCredentialRequests({ +// scopes: ["scopes"], +// credential, +// insecureResponses: [ +// // This time, no ping should be triggered +// createResponse(200, { access_token: "token", expires_on: "1506484173" }), +// ], +// }); +// assert.isUndefined(authDetails2.error); +// assert.equal(authDetails2.requests.length, 1); +// assert.equal(authDetails2.result?.token, "token"); +// }); + +// it("sends an authorization request correctly in an App Service environment", async () => { +// // Trigger App Service behavior by setting environment variables +// process.env.MSI_ENDPOINT = "https://endpoint"; +// process.env.MSI_SECRET = "secret"; + +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential: new LegacyMsiProvider("client"), +// secureResponses: [ +// createResponse(200, { +// access_token: "token", +// expires_on: "06/20/2019 02:57:58 +00:00", +// }), +// ], +// }); + +// const authRequest = authDetails.requests[0]; +// const query = new URLSearchParams(authRequest.url.split("?")[1]); + +// assert.equal(authRequest.method, "GET"); +// assert.equal(query.get("clientid"), "client"); +// assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); +// assert.ok( +// authRequest.url.startsWith(process.env.MSI_ENDPOINT), +// "URL does not start with expected host and path", +// ); +// assert.equal(authRequest.headers.secret, process.env.MSI_SECRET); +// assert.ok( +// authRequest.url.indexOf(`api-version=2017-09-01`) > -1, +// "URL does not have expected version", +// ); +// if (authDetails.result?.token) { +// assert.equal(authDetails.result.expiresOnTimestamp, 1560999478000); +// } else { +// assert.fail("No token was returned!"); +// } +// }); + +// it("sends an authorization request correctly in an App Service 2019 environment by client id", async () => { +// // Trigger App Service behavior by setting environment variables +// process.env.IDENTITY_ENDPOINT = "https://endpoint"; +// process.env.IDENTITY_HEADER = "HEADER"; + +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential: new LegacyMsiProvider("client"), +// secureResponses: [ +// createResponse(200, { +// access_token: "token", +// expires_on: "1624157878", +// }), +// ], +// }); + +// const authRequest = authDetails.requests[0]; +// const query = new URLSearchParams(authRequest.url.split("?")[1]); + +// assert.equal(authRequest.method, "GET"); +// assert.equal(query.get("client_id"), "client"); +// assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); +// assert.ok( +// authRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), +// "URL does not start with expected host and path", +// ); +// assert.equal(authRequest.headers["X-IDENTITY-HEADER"], process.env.IDENTITY_HEADER); +// assert.ok( +// authRequest.url.indexOf(`api-version=2019-08-01`) > -1, +// "URL does not have expected version", +// ); +// if (authDetails.result?.token) { +// assert.equal(authDetails.result.expiresOnTimestamp, 1624157878000); +// } else { +// assert.fail("No token was returned!"); +// } +// }); + +// it("sends an authorization request correctly in an App Service 2019 environment by resource id", async () => { +// // Trigger App Service behavior by setting environment variables +// process.env.IDENTITY_ENDPOINT = "https://endpoint"; +// process.env.IDENTITY_HEADER = "HEADER"; + +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential: new LegacyMsiProvider({ resourceId: "RESOURCE-ID" }), +// secureResponses: [ +// createResponse(200, { +// access_token: "token", +// expires_on: "1624157878", +// }), +// ], +// }); + +// const authRequest = authDetails.requests[0]; +// const query = new URLSearchParams(authRequest.url.split("?")[1]); + +// assert.equal(authRequest.method, "GET"); +// assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); +// assert.equal(decodeURIComponent(query.get("mi_res_id")!), "RESOURCE-ID"); +// assert.ok( +// authRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), +// "URL does not start with expected host and path", +// ); +// assert.equal(authRequest.headers["X-IDENTITY-HEADER"], process.env.IDENTITY_HEADER); +// assert.ok( +// authRequest.url.indexOf(`api-version=2019-08-01`) > -1, +// "URL does not have expected version", +// ); +// if (authDetails.result?.token) { +// assert.equal(authDetails.result.expiresOnTimestamp, 1624157878000); +// } else { +// assert.fail("No token was returned!"); +// } +// }); + +// it("sends an authorization request correctly in an Cloud Shell environment", async () => { +// // Trigger Cloud Shell behavior by setting environment variables +// process.env.MSI_ENDPOINT = "https://endpoint"; +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential: new LegacyMsiProvider(), +// secureResponses: [ +// createResponse(200, { +// access_token: "token", +// expires_in: "4310", +// expires_on: "1663366555", +// }), +// ], +// }); +// const authRequest = authDetails.requests[0]; +// assert.equal(authRequest.method, "POST"); +// assert.equal(authDetails.result!.token, "token"); +// }); + +// it("sends an authorization request correctly in an Cloud Shell environment (with clientId)", async () => { +// // Trigger Cloud Shell behavior by setting environment variables +// process.env.MSI_ENDPOINT = "https://endpoint"; +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential: new LegacyMsiProvider({ clientId: "CLIENT-ID" }), +// secureResponses: [ +// createResponse(200, { +// access_token: "token", +// expires_in: "4310", +// expires_on: "1663366555", +// }), +// ], +// }); +// const authRequest = authDetails.requests[0]; +// const body = new URLSearchParams(authRequest.body); +// assert.strictEqual(decodeURIComponent(body.get("client_id")!), "CLIENT-ID"); +// assert.equal(authRequest.method, "POST"); +// assert.equal(authDetails.result!.token, "token"); +// }); + +// it("sends an authorization request correctly in an Cloud Shell environment (with resourceId)", async () => { +// // Trigger Cloud Shell behavior by setting environment variables +// process.env.MSI_ENDPOINT = "https://endpoint"; +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential: new LegacyMsiProvider({ resourceId: "RESOURCE-ID" }), +// secureResponses: [ +// createResponse(200, { +// access_token: "token", +// expires_in: "4310", +// expires_on: "1663366555", +// }), +// ], +// }); +// const authRequest = authDetails.requests[0]; +// const body = new URLSearchParams(authRequest.body); +// assert.strictEqual(decodeURIComponent(body.get("msi_res_id")!), "RESOURCE-ID"); +// assert.equal(authRequest.method, "POST"); +// assert.equal(authDetails.result!.token, "token"); +// }); + +// it("authorization request fails with client id passed in an Cloud Shell environment", async function (this: Context) { +// // Trigger Cloud Shell behavior by setting environment variables +// process.env.MSI_ENDPOINT = "https://endpoint"; +// const msiGetTokenSpy = Sinon.spy(LegacyMsiProvider.prototype, "getToken"); +// const loggerSpy = Sinon.spy(logger, "warning"); +// setLogLevel("warning"); +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential: new LegacyMsiProvider("client"), +// secureResponses: [ +// createResponse(200, { +// access_token: "token", +// expires_in: "4310", +// expires_on: "1663366555", +// }), +// ], +// }); +// assert.equal(authDetails.result!.token, "token"); +// assert.equal(msiGetTokenSpy.called, true); +// assert.equal(loggerSpy.calledOnce, true); +// assert.deepEqual(loggerSpy.args[0], [ +// "ManagedIdentityCredential - CloudShellMSI: user-assigned identities not supported. The argument clientId might be ignored by the service.", +// ]); +// }); + +// describe("Azure Arc", function () { +// const keyContents = "challenge key"; +// let expectedDirectory: string; + +// beforeEach(function () { +// if (process.platform !== "win32" && process.platform !== "linux") { +// // not supported on this platform +// this.skip(); +// } +// expectedDirectory = arcMsi.platformToFilePath(); + +// // Trigger Azure Arc behavior by setting environment variables +// process.env.IMDS_ENDPOINT = "http://endpoint"; +// process.env.IDENTITY_ENDPOINT = "http://endpoint"; +// // Stub out a valid key file +// Sinon.stub(fs, "statSync").returns({ size: 400 } as any); +// Sinon.stub(fs.promises, "readFile").resolves(keyContents); +// }); + +// afterEach(function () { +// Sinon.restore(); +// }); + +// it("sends an authorization request correctly in an Azure Arc environment", async function (this: Mocha.Context) { +// const tempFile = join(expectedDirectory, "fake.key"); + +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential: new LegacyMsiProvider(), +// insecureResponses: [ +// createResponse( +// 401, +// {}, +// { +// "www-authenticate": `we don't pay much attention about this format=${tempFile}`, +// }, +// ), +// createResponse(200, { +// access_token: "token", +// expires_in: 1, +// }), +// ], +// }); + +// // File request +// const validationRequest = authDetails.requests[0]; +// let query = new URLSearchParams(validationRequest.url.split("?")[1]); + +// assert.equal(validationRequest.method, "GET"); +// assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); + +// assert.exists(process.env.IDENTITY_ENDPOINT); +// assert.ok( +// validationRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), +// "URL does not start with expected host and path", +// ); + +// // Authorization request, which comes after getting the file path, for now at least. +// const authRequest = authDetails.requests[1]; +// query = new URLSearchParams(authRequest.url.split("?")[1]); + +// assert.equal(authRequest.method, "GET"); +// assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); + +// assert.ok( +// authRequest.url.startsWith(process.env.IDENTITY_ENDPOINT!), +// "URL does not start with expected host and path", +// ); + +// assert.equal(authRequest.headers.Authorization, `Basic ${keyContents}`); +// if (authDetails.result!.token) { +// // We use Date.now underneath. +// assert.ok(authDetails.result!.expiresOnTimestamp); +// } else { +// assert.fail("No token was returned!"); +// } +// }); + +// it("sends an authorization request correctly in an Azure Arc environment (with resourceId)", async function (this: Mocha.Context) { +// const filePath = join(expectedDirectory, "fake.key"); + +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential: new LegacyMsiProvider({ resourceId: "RESOURCE-ID" }), +// insecureResponses: [ +// createResponse( +// 401, +// {}, +// { +// "www-authenticate": `we don't pay much attention about this format=${filePath}`, +// }, +// ), +// createResponse(200, { +// access_token: "token", +// expires_in: 1, +// }), +// ], +// }); + +// // File request +// const validationRequest = authDetails.requests[0]; +// let query = new URLSearchParams(validationRequest.url.split("?")[1]); + +// assert.equal(validationRequest.method, "GET"); +// assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); + +// assert.exists(process.env.IDENTITY_ENDPOINT); +// assert.ok( +// validationRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), +// "URL does not start with expected host and path", +// ); + +// // Authorization request, which comes after getting the file path, for now at least. +// const authRequest = authDetails.requests[1]; +// query = new URLSearchParams(authRequest.url.split("?")[1]); + +// assert.equal(authRequest.method, "GET"); +// assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); +// assert.equal(decodeURIComponent(query.get("msi_res_id")!), "RESOURCE-ID"); + +// assert.ok( +// authRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), +// "URL does not start with expected host and path", +// ); + +// assert.equal(authRequest.headers.Authorization, `Basic ${keyContents}`); +// if (authDetails.result!.token) { +// // We use Date.now underneath. +// assert.ok(authDetails.result!.expiresOnTimestamp); +// } else { +// assert.fail("No token was returned!"); +// } +// }); + +// it("sends an authorization request correctly in an Azure Arc environment (with clientId)", async function (this: Mocha.Context) { +// const filePath = join(expectedDirectory, "fake.key"); + +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential: new LegacyMsiProvider({ clientId: "CLIENT-ID" }), +// insecureResponses: [ +// createResponse( +// 401, +// {}, +// { +// "www-authenticate": `we don't pay much attention about this format=${filePath}`, +// }, +// ), +// createResponse(200, { +// access_token: "token", +// expires_in: 1, +// }), +// ], +// }); + +// // File request +// const validationRequest = authDetails.requests[0]; +// console.log(validationRequest.url.split("?")[1]); +// let query = new URLSearchParams(validationRequest.url.split("?")[1]); + +// assert.equal(validationRequest.method, "GET"); +// assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); + +// assert.exists(process.env.IDENTITY_ENDPOINT); +// assert.ok( +// validationRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), +// "URL does not start with expected host and path", +// ); + +// // Authorization request, which comes after getting the file path, for now at least. +// const authRequest = authDetails.requests[1]; +// console.log(authRequest.url.split("?")[1]); +// query = new URLSearchParams(authRequest.url.split("?")[1]); + +// assert.equal(authRequest.method, "GET"); +// assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); +// assert.equal(decodeURIComponent(query.get("client_id")!), "CLIENT-ID"); + +// assert.ok( +// authRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), +// "URL does not start with expected host and path", +// ); + +// assert.equal(authRequest.headers.Authorization, `Basic ${keyContents}`); +// if (authDetails.result!.token) { +// // We use Date.now underneath. +// assert.ok(authDetails.result!.expiresOnTimestamp); +// } else { +// assert.fail("No token was returned!"); +// } +// }); +// }); + +// it("sends an authorization request correctly in an Azure Fabric environment", async () => { +// // Trigger App Service behavior by setting environment variables +// process.env.IDENTITY_ENDPOINT = "https://endpoint"; +// process.env.IDENTITY_HEADER = "secret"; + +// // We're not verifying the certificate yet, but we still check for it: +// process.env.IDENTITY_SERVER_THUMBPRINT = "certificate-thumbprint"; + +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential: new LegacyMsiProvider("client"), +// secureResponses: [ +// createResponse(200, { +// access_token: "token", +// expires_on: 1, +// }), +// ], +// }); + +// // Authorization request, which comes after validating again, for now at least. +// const authRequest = authDetails.requests[0]; + +// const query = new URLSearchParams(authRequest.url.split("?")[1]); + +// assert.equal(authRequest.method, "GET"); +// assert.equal(query.get("client_id"), "client"); +// assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); +// assert.ok( +// authRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), +// "URL does not start with expected host and path", +// ); + +// assert.equal(authRequest.headers.secret, process.env.IDENTITY_HEADER); + +// if (authDetails.result!.token) { +// // We use Date.now underneath. +// assert.equal(authDetails.result!.expiresOnTimestamp, 1000); +// } else { +// assert.fail("No token was returned!"); +// } +// }); + +// it("sends an authorization request correctly in an Azure Fabric environment (resourceId)", async () => { +// // Trigger App Service behavior by setting environment variables +// process.env.IDENTITY_ENDPOINT = "https://endpoint"; +// process.env.IDENTITY_HEADER = "secret"; + +// // We're not verifying the certificate yet, but we still check for it: +// process.env.IDENTITY_SERVER_THUMBPRINT = "certificate-thumbprint"; + +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential: new LegacyMsiProvider({ resourceId: "RESOURCE-ID" }), +// secureResponses: [ +// createResponse(200, { +// access_token: "token", +// expires_on: 1, +// }), +// ], +// }); + +// // Authorization request, which comes after validating again, for now at least. +// const authRequest = authDetails.requests[0]; + +// const query = new URLSearchParams(authRequest.url.split("?")[1]); + +// assert.equal(authRequest.method, "GET"); +// assert.equal(query.get("msi_res_id"), "RESOURCE-ID"); +// assert.equal(decodeURIComponent(query.get("resource")!), "https://service"); +// assert.ok( +// authRequest.url.startsWith(process.env.IDENTITY_ENDPOINT), +// "URL does not start with expected host and path", +// ); + +// assert.equal(authRequest.headers.secret, process.env.IDENTITY_HEADER); + +// if (authDetails.result!.token) { +// // We use Date.now underneath. +// assert.equal(authDetails.result!.expiresOnTimestamp, 1000); +// } else { +// assert.fail("No token was returned!"); +// } +// }); + +// it("calls to AppTokenProvider for MI token caching support", async () => { +// const credential: any = new LegacyMsiProvider("client"); +// const confidentialSpy = Sinon.spy(credential.confidentialApp, "SetAppTokenProvider"); + +// // Trigger App Service behavior by setting environment variables +// process.env.MSI_ENDPOINT = "https://endpoint"; +// process.env.MSI_SECRET = "secret"; + +// const authDetails = await testContext.sendCredentialRequests({ +// scopes: ["https://service/.default"], +// credential, +// secureResponses: [ +// createResponse(200, { +// access_token: "token", +// expires_on: "06/20/2019 02:57:58 +00:00", +// }), +// ], +// }); +// assert.equal(confidentialSpy.callCount, 1); + +// if (authDetails.result?.token) { +// assert.equal(authDetails.result.expiresOnTimestamp, 1560999478000); +// } else { +// assert.fail("No token was returned!"); +// } +// }); +// }); diff --git a/sdk/identity/identity/test/internal/node/managedIdentityCredential/msalMsiProvider.spec.ts b/sdk/identity/identity/test/internal/node/managedIdentityCredential/msalMsiProvider.spec.ts index 584d0c57e972..913f8c9135ad 100644 --- a/sdk/identity/identity/test/internal/node/managedIdentityCredential/msalMsiProvider.spec.ts +++ b/sdk/identity/identity/test/internal/node/managedIdentityCredential/msalMsiProvider.spec.ts @@ -5,7 +5,7 @@ import Sinon from "sinon"; import { assert } from "@azure-tools/test-utils"; import type { AuthenticationResult } from "@azure/msal-node"; import { AuthError, ManagedIdentityApplication } from "@azure/msal-node"; -import { MsalMsiProvider } from "../../../../src/credentials/managedIdentityCredential/msalMsiProvider"; +import { ManagedIdentityCredential } from "../../../../src/credentials/managedIdentityCredential/index"; import { tokenExchangeMsi } from "../../../../src/credentials/managedIdentityCredential/tokenExchangeMsi"; import { imdsMsi } from "../../../../src/credentials/managedIdentityCredential/imdsMsi"; import { RestError } from "@azure/core-rest-pipeline"; @@ -38,28 +38,28 @@ describe("ManagedIdentityCredential (MSAL)", function () { // by relying on the error handling of the constructor it("throws when both clientId and resourceId are provided", function () { assert.throws( - () => new MsalMsiProvider("id", { resourceId: "id" }), + () => new ManagedIdentityCredential("id", { resourceId: "id" } as any), /only one of 'clientId', 'resourceId', or 'objectId' can be provided/, ); }); it("throws when both clientId and resourceId are provided via options", function () { assert.throws( - () => new MsalMsiProvider({ clientId: "id", resourceId: "id" }), + () => new ManagedIdentityCredential({ clientId: "id", resourceId: "id" } as any), /only one of 'clientId', 'resourceId', or 'objectId' can be provided/, ); }); it("throws when both clientId and objectId are provided", function () { assert.throws( - () => new MsalMsiProvider("id", { objectId: "id" }), + () => new ManagedIdentityCredential("id", { objectId: "id" } as any), /only one of 'clientId', 'resourceId', or 'objectId' can be provided/, ); }); it("throws when both resourceId and objectId are provided via options", function () { assert.throws( - () => new MsalMsiProvider({ resourceId: "id", objectId: "id" }), + () => new ManagedIdentityCredential({ resourceId: "id", objectId: "id" } as any), /only one of 'clientId', 'resourceId', or 'objectId' can be provided/, ); }); @@ -72,15 +72,15 @@ describe("ManagedIdentityCredential (MSAL)", function () { ); assert.throws( - () => new MsalMsiProvider({ clientId: "id" }), + () => new ManagedIdentityCredential({ clientId: "id" }), /Specifying a user-assigned managed identity is not supported for CloudShell at runtime/, ); assert.throws( - () => new MsalMsiProvider({ resourceId: "id" }), + () => new ManagedIdentityCredential({ resourceId: "id" }), /Specifying a user-assigned managed identity is not supported for CloudShell at runtime/, ); assert.throws( - () => new MsalMsiProvider({ objectId: "id" }), + () => new ManagedIdentityCredential({ objectId: "id" }), /Specifying a user-assigned managed identity is not supported for CloudShell at runtime/, ); }); @@ -91,8 +91,8 @@ describe("ManagedIdentityCredential (MSAL)", function () { describe("when getToken is successful", function () { it("returns a token", async function () { acquireTokenStub.resolves(validAuthenticationResult as AuthenticationResult); - const provider = new MsalMsiProvider(); - const token = await provider.getToken("scope"); + const credential = new ManagedIdentityCredential(); + const token = await credential.getToken("scope"); assert.strictEqual(token.token, validAuthenticationResult.accessToken); assert.strictEqual( token.expiresOnTimestamp, @@ -110,8 +110,8 @@ describe("ManagedIdentityCredential (MSAL)", function () { Sinon.stub(tokenExchangeMsi, "isAvailable").resolves(true); Sinon.stub(tokenExchangeMsi, "getToken").resolves(validToken); - const provider = new MsalMsiProvider(); - const token = await provider.getToken("scope"); + const credential = new ManagedIdentityCredential(); + const token = await credential.getToken("scope"); assert.strictEqual(token.token, validToken.token); assert.strictEqual(token.expiresOnTimestamp, validToken.expiresOnTimestamp); }); @@ -124,29 +124,29 @@ describe("ManagedIdentityCredential (MSAL)", function () { ); acquireTokenStub.resolves(validAuthenticationResult as AuthenticationResult); - const provider = new MsalMsiProvider(); - await provider.getToken("scope"); + const credential = new ManagedIdentityCredential(); + await credential.getToken("scope"); assert.isTrue(imdsIsAvailableStub.calledOnce); }); }); }); it("validates multiple scopes are not supported", async function () { - const provider = new MsalMsiProvider(); - await assert.isRejected(provider.getToken(["scope1", "scope2"]), /Multiple scopes/); + const credential = new ManagedIdentityCredential(); + await assert.isRejected(credential.getToken(["scope1", "scope2"]), /Multiple scopes/); }); describe("error handling", function () { it("rethrows AuthenticationRequiredError", async function () { acquireTokenStub.rejects(new AuthenticationRequiredError({ scopes: ["scope"] })); - const provider = new MsalMsiProvider(); - await assert.isRejected(provider.getToken("scope"), AuthenticationRequiredError); + const credential = new ManagedIdentityCredential(); + await assert.isRejected(credential.getToken("scope"), AuthenticationRequiredError); }); it("handles an unreachable network error", async function () { acquireTokenStub.rejects(new AuthError("network_error")); - const provider = new MsalMsiProvider(); - await assert.isRejected(provider.getToken("scope"), CredentialUnavailableError); + const credential = new ManagedIdentityCredential(); + await assert.isRejected(credential.getToken("scope"), CredentialUnavailableError); }); it("handles a 403 status code", async function () { @@ -155,14 +155,14 @@ describe("ManagedIdentityCredential (MSAL)", function () { statusCode: 403, }), ); - const provider = new MsalMsiProvider(); - await assert.isRejected(provider.getToken("scope"), /Network unreachable/); + const credential = new ManagedIdentityCredential(); + await assert.isRejected(credential.getToken("scope"), /Network unreachable/); }); it("handles unexpected errors", async function () { acquireTokenStub.rejects(new Error("Some unexpected error")); - const provider = new MsalMsiProvider(); - await assert.isRejected(provider.getToken("scope"), /Authentication failed/); + const credential = new ManagedIdentityCredential(); + await assert.isRejected(credential.getToken("scope"), /Authentication failed/); }); }); });