// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import { SpanStatusCode } from "@azure/core-tracing";
import { IdentityClient } from "../../client/identityClient";
import { AuthenticationError, CredentialUnavailableError } from "../../errors";
import { credentialLogger, formatSuccess, formatError } from "../../util/logging";
import { appServiceMsi2017 } from "./appServiceMsi2017";
import { createSpan } from "../../util/tracing";
import { cloudShellMsi } from "./cloudShellMsi";
import { imdsMsi } from "./imdsMsi";
import { arcMsi } from "./arcMsi";
import { tokenExchangeMsi } from "./tokenExchangeMsi";
import { fabricMsi } from "./fabricMsi";
const logger = credentialLogger("ManagedIdentityCredential");
/**
 * Attempts authentication using a managed identity available at the deployment environment.
 * This authentication type works in Azure VMs, App Service instances, Azure Functions applications,
 * Azure Kubernetes Services, Azure Service Fabric instances and inside of the Azure Cloud Shell.
 *
 * More information about configuring managed identities can be found here:
 * https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview
 */
export class ManagedIdentityCredential {
    /**
     * @internal
     * @hidden
     */
    constructor(clientIdOrOptions, options) {
        this.isEndpointUnavailable = null;
        let _options;
        if (typeof clientIdOrOptions === "string") {
            // clientId, options constructor
            this.clientId = clientIdOrOptions;
            _options = options;
        }
        else {
            // options only constructor
            _options = clientIdOrOptions;
        }
        this.identityClient = new IdentityClient(_options);
        this.isAvailableIdentityClient = new IdentityClient(Object.assign(Object.assign({}, _options), { retryOptions: {
                maxRetries: 0,
            } }));
    }
    async cachedAvailableMSI(scopes, clientId, getTokenOptions) {
        if (this.cachedMSI) {
            return this.cachedMSI;
        }
        const MSIs = [fabricMsi, appServiceMsi2017, cloudShellMsi, arcMsi, tokenExchangeMsi(), imdsMsi];
        for (const msi of MSIs) {
            if (await msi.isAvailable(scopes, this.isAvailableIdentityClient, clientId, getTokenOptions)) {
                this.cachedMSI = msi;
                return msi;
            }
        }
        throw new CredentialUnavailableError(`${ManagedIdentityCredential.name} - No MSI credential available`);
    }
    async authenticateManagedIdentity(scopes, clientId, getTokenOptions) {
        const { span, updatedOptions } = createSpan(`${ManagedIdentityCredential.name}.authenticateManagedIdentity`, getTokenOptions);
        try {
            // Determining the available MSI, and avoiding checking for other MSIs while the program is running.
            const availableMSI = await this.cachedAvailableMSI(scopes, clientId, updatedOptions);
            return availableMSI.getToken({
                identityClient: this.identityClient,
                scopes,
                clientId,
            }, updatedOptions);
        }
        catch (err) {
            span.setStatus({
                code: SpanStatusCode.ERROR,
                message: err.message,
            });
            throw err;
        }
        finally {
            span.end();
        }
    }
    /**
     * Authenticates with Azure Active Directory and returns an access token if successful.
     * If authentication fails, a {@link CredentialUnavailableError} will be thrown with the details of the failure.
     * If an unexpected error occurs, an {@link AuthenticationError} will be thrown with the details of the failure.
     *
     * @param scopes - The list of scopes for which the token will have access.
     * @param options - The options used to configure any requests this
     *                TokenCredential implementation might make.
     */
    async getToken(scopes, options) {
        let result = null;
        const { span, updatedOptions } = createSpan(`${ManagedIdentityCredential.name}.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) {
                result = await this.authenticateManagedIdentity(scopes, this.clientId, updatedOptions);
                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) {
            // 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({
                code: SpanStatusCode.ERROR,
                message: err.message,
            });
            // If either the network is unreachable,
            // we can safely assume the credential is unavailable.
            if (err.code === "ENETUNREACH") {
                const error = new CredentialUnavailableError(`${ManagedIdentityCredential.name}: 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.name}: 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.name}: The managed identity endpoint is indicating there's no available identity. Message: ${err.message}`);
            }
            // 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.name}: Authentication failed. Message ${err.message}`);
            }
            // Any other error should break the chain.
            throw new AuthenticationError(err.statusCode, {
                error: `${ManagedIdentityCredential.name} 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();
        }
    }
}
//# sourceMappingURL=index.js.map